FieldSelect
Поле выбора из списка. Базируется на FieldDecorator и Droplist из @cloud-ru/ds-list. Поддерживает выбор одного значения или нескольких (с чипами), поиск по списку, кнопки очистки и копирования (последняя — только в readonly).
Когда использовать
- Выбор одного значения из фиксированного списка (размер инстанса, образ ОС, валюта).
- Выбор нескольких значений с отображением чипов (
selection='multiple'). - Когда список длинный и нужен поиск по подстроке или нечёткий поиск (
searchable,enableFuzzySearch). - Для свободного ввода с подсказками (suggest, recent) используйте
@cloud-ru/ds-search, не FieldSelect.
Анатомия
Size (default m)
Высота поля задаётся размером: s — 40px, m — 52px, l — 66px.
| Значение | Когда |
|---|---|
s | Плотные таблицы, inline-редактирование (высота 40px) |
m | Стандартные формы, по умолчанию (высота 52px) |
l | Лендинги, primary-формы (высота 66px) |
ValidationState (default default)
Управляет тонировкой фона и иконкой подсказки. Проп error форсит error.
| Значение | Когда |
|---|---|
default | Нет валидации — без иконки и тонировки |
error | Поле не прошло валидацию (красная тонировка) |
warning | Предупреждение, выбор допустим (жёлтая тонировка) |
success | Подтверждение успешного выбора (зелёная тонировка) |
Selection (default single)
| Значение | Когда |
|---|---|
single | Одно значение, поле показывает его лейбл (по умолчанию) |
multiple | Несколько значений; при chips=true — чипы внутри поля, иначе строка через , |
Поведение
| Проп | Поведение |
|---|---|
searchable | Включает ввод в поле; список фильтруется по лейблу (default true) |
enableFuzzySearch | Нечёткий поиск — символы запроса в любом порядке (default true); false — substring |
search | Управляемая строка поиска ({ value, defaultValue, onChange }) — для серверного поиска вместе с autocomplete |
autocomplete | Не фильтровать список на клиенте: фильтрацию обеспечивает потребитель (серверный поиск), default false |
addOptionByEnter | По Enter зафиксировать введённый текст как новый выбор (создание опции «на лету»), default false |
resetSearchOnOptionSelection | Сбрасывать строку поиска к выбранному значению после выбора (default true); false — при асинхронной подгрузке |
showClearButton | Кнопка очистки (✕) при выбранном значении и активном поле (default true) |
showCopyButton | Кнопка копирования значения — только в readonly при непустом значении (default true) |
removeByBackspace | В multiple + chips удаляет последний чип по Backspace при пустом вводе (default true) |
disabled-айтем | Чип отключённого значения не получает кнопку удаления и не сбрасывается при очистке |
Установка
pnpm add @cloud-ru/ds-fields
import { FieldSelect } from '@cloud-ru/ds-fields'
Примеры использования
Выбор одного значения
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
const options: ItemProps[] = [
{ id: 's', content: { label: 'Small (1 vCPU, 2 GB)' } },
{ id: 'm', content: { label: 'Medium (2 vCPU, 4 GB)' } },
{ id: 'l', content: { label: 'Large (4 vCPU, 8 GB)' } },
{ id: 'xl', content: { label: 'X-Large (8 vCPU, 16 GB)' } },
];
export function Select() {
const [value, setValue] = useState<ItemId | undefined>('m');
return (
<FieldSelect
label='Размер инстанса'
placeholder='Выберите размер'
selection='single'
items={options}
value={value}
onChange={setValue}
/>
);
}Множественный выбор с чипами
ru-central1-aru-central1-b
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
const options: ItemProps[] = [
{ id: 'ru-1', content: { label: 'ru-central1-a' } },
{ id: 'ru-2', content: { label: 'ru-central1-b' } },
{ id: 'ru-3', content: { label: 'ru-central1-c' } },
{ id: 'kz-1', content: { label: 'kz-central1-a' } },
];
export function SelectMultiple() {
const [value, setValue] = useState<ItemId[]>(['ru-1', 'ru-2']);
return (
<FieldSelect
label='Зоны доступности'
placeholder='Выберите зоны'
selection='multiple'
chips
items={options}
value={value}
onChange={setValue}
/>
);
}Поиск по списку
Нечёткий поиск: «aple» найдёт «Alpine Linux»
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
const options: ItemProps[] = [
{ id: 'ubuntu', content: { label: 'Ubuntu 22.04 LTS' } },
{ id: 'debian', content: { label: 'Debian 12' } },
{ id: 'centos', content: { label: 'CentOS Stream 9' } },
{ id: 'alpine', content: { label: 'Alpine Linux 3.19' } },
{ id: 'rocky', content: { label: 'Rocky Linux 9' } },
];
export function SelectSearchable() {
const [value, setValue] = useState<ItemId | undefined>(undefined);
return (
<FieldSelect
label='Образ ОС'
placeholder='Начните вводить название'
hint='Нечёткий поиск: «aple» найдёт «Alpine Linux»'
selection='single'
searchable
enableFuzzySearch
items={options}
value={value}
onChange={setValue}
/>
);
}Readonly с копированием
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemProps } from '@cloud-ru/ds-list';
const options: ItemProps[] = [
{ id: 'm', content: { label: 'Medium (2 vCPU, 4 GB)' } },
{ id: 'l', content: { label: 'Large (4 vCPU, 8 GB)' } },
];
export function SelectReadonly() {
return <FieldSelect label='Размер инстанса' readonly selection='single' items={options} defaultValue='l' />;
}Защита отключённых чипов
ЧтениеЗапись
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
const options: ItemProps[] = [
{ id: 'read', content: { label: 'Чтение' }, disabled: true },
{ id: 'write', content: { label: 'Запись' } },
{ id: 'delete', content: { label: 'Удаление' } },
{ id: 'admin', content: { label: 'Администрирование' } },
];
export function SelectDisabledChips() {
// Право «Чтение» обязательно: его чип не получает кнопку удаления и не сбрасывается при очистке.
const [value, setValue] = useState<ItemId[]>(['read', 'write']);
return (
<FieldSelect
label='Права доступа'
placeholder='Выберите права'
selection='multiple'
chips
items={options}
value={value}
onChange={setValue}
/>
);
}Серверный поиск (autocomplete)
autocomplete: фильтрует потребитель (серверный поиск), не сам компонент
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useMemo, useState } from 'react';
const ALL_REGIONS: ItemProps[] = [
{ id: 'ru-moscow', content: { label: 'Москва' } },
{ id: 'ru-spb', content: { label: 'Санкт-Петербург' } },
{ id: 'ru-novosibirsk', content: { label: 'Новосибирск' } },
{ id: 'ru-ekaterinburg', content: { label: 'Екатеринбург' } },
{ id: 'ru-kazan', content: { label: 'Казань' } },
];
export function SelectAutocomplete() {
const [value, setValue] = useState<ItemId | undefined>(undefined);
const [query, setQuery] = useState('');
// Фильтрацию выполняет потребитель (имитация backend-поиска): FieldSelect с `autocomplete`
// не фильтрует список повторно, а строка поиска управляется через `search`.
const items = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return ALL_REGIONS;
return ALL_REGIONS.filter(item => {
const content = 'content' in item ? item.content : undefined;
const option = content && typeof content === 'object' && 'label' in content ? String(content.label) : '';
return option.toLowerCase().includes(q);
});
}, [query]);
return (
<FieldSelect
label='Регион'
placeholder='Начните вводить название'
hint='autocomplete: фильтрует потребитель (серверный поиск), не сам компонент'
selection='single'
autocomplete
search={{ value: query, onChange: setQuery }}
items={items}
value={value}
onChange={setValue}
/>
);
}Создание значения по Enter
frontend
addOptionByEnter: введённый текст по Enter становится новым выбранным значением (чипом)
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
const PRESET_TAGS: ItemProps[] = [
{ id: 'frontend', content: { label: 'frontend' } },
{ id: 'backend', content: { label: 'backend' } },
{ id: 'infra', content: { label: 'infra' } },
];
export function SelectCreatable() {
const [value, setValue] = useState<ItemId[]>(['frontend']);
return (
<FieldSelect
label='Теги'
placeholder='Введите тег и нажмите Enter'
hint='addOptionByEnter: введённый текст по Enter становится новым выбранным значением (чипом)'
selection='multiple'
addOptionByEnter
items={PRESET_TAGS}
value={value}
onChange={setValue}
/>
);
}Закреплённые опции
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
const regions: ItemProps[] = [
{ id: 'ru-moscow-1', content: { label: 'ru-moscow-1' } },
{ id: 'ru-moscow-2', content: { label: 'ru-moscow-2' } },
{ id: 'kz-ala-1', content: { label: 'kz-ala-1' } },
{ id: 'gis-tomsk-1', content: { label: 'gis-tomsk-1' } },
];
export function SelectPinned() {
const [value, setValue] = useState<ItemId | undefined>('ru-moscow-1');
return (
<FieldSelect
label='Регион'
placeholder='Выберите регион'
selection='single'
searchable
items={regions}
pinTop={[{ id: 'recommended', content: { label: 'ru-moscow-1', caption: 'Рекомендуемый' } }]}
pinBottom={[{ id: 'all-regions', content: { label: 'Показать все регионы' } }]}
value={value}
onChange={setValue}
/>
);
}Опции с описанием
Поиск ищет по названию, характеристикам и описанию
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
// Айтемы @cloud-ru/ds-list поддерживают label + caption + description.
// Поиск (extractSearchText) матчит запрос по всем трём полям.
const options: ItemProps[] = [
{
id: 's',
content: { label: 'Small', caption: '1 vCPU · 2 GB', description: 'Для dev-окружений и небольших сервисов' },
},
{
id: 'm',
content: { label: 'Medium', caption: '2 vCPU · 4 GB', description: 'Стандартная нагрузка, веб-приложения' },
},
{
id: 'l',
content: { label: 'Large', caption: '4 vCPU · 8 GB', description: 'Базы данных, аналитика, очереди' },
},
];
export function SelectRichContent() {
const [value, setValue] = useState<ItemId | undefined>('m');
return (
<FieldSelect
label='Размер инстанса'
placeholder='Выберите размер'
hint='Поиск ищет по названию, характеристикам и описанию'
selection='single'
searchable
items={options}
value={value}
onChange={setValue}
/>
);
}Обёртка айтема (itemWrapRender)
Каждый айтем обёрнут в навигационную ссылку через itemWrapRender
tsx
import { FieldSelect } from '@cloud-ru/ds-fields';
import { ItemId, ItemProps } from '@cloud-ru/ds-list';
import { useState } from 'react';
// itemWrapRender оборачивает отрендеренный айтем в произвольный узел — типичный кейс:
// проксирование в навигационную ссылку (<a> / Link роутера).
const options: ItemProps[] = [
{
id: 'docs',
content: { label: 'Документация' },
itemWrapRender: node => <a href='/docs'>{node}</a>,
},
{
id: 'api',
content: { label: 'API Reference' },
itemWrapRender: node => (
<a href='https://example.com/api' target='_blank' rel='noopener noreferrer'>
{node}
</a>
),
},
];
export function SelectItemWrapRender() {
const [value, setValue] = useState<ItemId | undefined>(undefined);
return (
<FieldSelect
label='Раздел'
placeholder='Выберите раздел'
hint='Каждый айтем обёрнут в навигационную ссылку через itemWrapRender'
selection='single'
items={options}
value={value}
onChange={setValue}
/>
);
}Props
Types
Props
FieldSelectProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
addOptionByEnter | boolean | false | no | Зафиксировать введённый текст как новый выбор по `Enter` (создание опции «на лету»). |
autoFocus | boolean | — | no | Автофокус input при монтировании. На mobile выключается адаптивно (см. `layoutPresets`) |
autocomplete | boolean | false | no | Не фильтровать список на клиенте — фильтрацию обеспечивает потребитель (серверный поиск). Введённый текст уходит в `search.onChange`, список берётся из `items` как есть. |
background | boolean | true | no | Фон поля (acrylic) |
caption | string | — | no | Вторичная подпись справа |
chips | boolean | true | no | Отображать выбранные значения как чипы (`@cloud-ru/ds-tag`) внутри поля. Если `false`, показывает строку из `formatSelected` либо comma-joined. |
className | string | — | no | CSS-класс CSS-класс корня `FieldDecorator` |
closeDroplistOnItemClick | boolean | true
false | no | Закрывать дроплист после клика на айтем. |
closeOnPopstate | boolean | — | no | Закрывать ли поповер при переходе по истории браузера |
data-test-id | string | — | no | Тесто вый id корня |
dataError | boolean | — | no | Флаг «ошибка загрузки данных» — при `true` дроплист рендерит `errorDataState` вместо списка (для асинхронной подгрузки с провалившимся запросом). |
dataFiltered | boolean | — | no | Флаг «список отфильтрован» — при `true` и пустом результате дроплист рендерит `noResultsState`. По умолчанию выводится из строки поиска (`searchable` + ввод). |
defaultValue | ItemId | ItemId[] | — | no | Неуправляемое значение по умолчанию Неуправляемые значения по умолчанию |
disabled | boolean | — | no | Поле выключено Деактивировано |
enableFuzzySearch | boolean | true | no | Включить нечёткий поиск: символы запроса должны встречаться в лейбле в том же порядке (например, `lge` найдёт `Large`). Если `false` — простой substring-match. |
error | string | — | no | Ошибка (приоритетнее `hint`; форсит `validationState=error`) |
errorDataState | EmptyStateProps | — | no | Экран при ошибке запроса |
fieldClassName | string | — | no | CSS-класс оболочки поля |
footer | ReactNode ; | — | no | Кастомизируемый элемент в конце списка |
footerActiveElementsRefs | RefObject<HTMLElement>[] | — | no | Список ссылок на к астомные элементы, помещенные в специальную секцию внизу списка |
formatSelected | ((selected: { id: ItemId; label: string; }[]) => string) | — список лейблов через `, ` | no | Форматтер строки выбранных значений (используется, если `chips=false`). |
hint | string | — | no | Подсказка |
iconBefore | ReactNode | — | no | Иконка перед текстом |
id | string | — | no | HTML-атрибут `id` для input (и `for` у label) |
innerRef | Ref<HTMLDivElement> | — | no | Ref на корневой DOM-элемент |
items | Item[] | — | yes | Список айтемов дроплиста (формат `@cloud-ru/ds-list`) |
label | string | — | no | Заголовок |
labelFor | string | — | no | HTML-атрибут `for` для `<label>` |
labelTooltip | QuestionTooltipProps | — | no | Подсказка (question-tooltip) у заголовка |
layoutPresets | Partial<Record<LayoutType, Partial<{ autoFocus: boolean; }>>> | — | no | Переопределение адаптивных дефолтов по раскладке. Участвует `autoFocus`: на mobile он выключен (открывает клавиатуру без действия). Вернуть на mobile — `layoutPresets={{ mobile: { autoFocus: true } }}`. |
length | FieldLength | — | no | Счётчик длины `current/max` |
limitedScrollHeight | boolean | — | no | Ограничить максимальную высоту скролл-контейнера в зависимости от `size` |
loading | boolean | — | no | Флаг, отвечающий за состояние загрузки списка |
name | string | — | no | HTML-атрибут `name` для input |
noDataState | EmptyStateProps | — | no | Экран при отсутствии данных |
noResultsState | EmptyStateProps | — | no | Экран при отсутствии результатов поиска или фильтров |
onBlur | ((event: FocusEvent<HTMLInputElement, Element>) => void) | — | no | Колбек блюра input |
onChange | ((value: ItemId) => void) | ((value: ItemId[]) => void) | — | no | Колбек смены значения Колбек смены значений |
onCopyButtonClick | (() => void) | — | no | Колбек после копирования значения в буфер |
onFocus | ((event: FocusEvent<HTMLInputElement, Element>) => void) | — | no | Колбек фокуса input |
onKeyDown | ((event: KeyboardEvent<HTMLInputElement>) => void) | — | no | Колбек нажатия клавиши на input (вызывается после внутренней обработки навигации) |
onOpenChange | ((isOpen: boolean) => void) | — | no | Колбек смены открытия |
open | boolean | — | no | Управляемое открытие дроплиста |
pinBottom | Item[] | — | no | Пресет-айтемы снизу (формат `@cloud-ru/ds-list`) |
pinTop | Item[] | — | no | Пресет-айтемы сверху (формат `@cloud-ru/ds-list`) |
placeholder | string | — | no | Placeholder в триггере, когда нет выбранного значения |
placement | "bottom" | "bottom-end" | "bottom-start" | "left" | "left-end" | "left-start" | "right" | "right-end" | "right-start" | "top" | "top-end" | "top-start" | — | no | Placement дроплиста |
postfix | ReactNode | — | no | Постфикс — текст или нода после значения (Figma `postfix`) |
prefix | ReactNode | — | no | Префикс — текст или нода перед значением (Figma `prefix`) |
readonly | boolean | — | no | Только для чтения Read-only режим |
removeByBackspace | boolean | true | no | Удалять последний чип по `Backspace`, когда строка ввода пустая. Работает только при `chips=true` и `searchable=true`. |
required | boolean | — | no | Показать знак обязательности `*` |
resetSearchOnOptionSelection | boolean | true | no | Сбрасывать строку поиска к выбранному значению после выбора. `false` нужен при асинхронной подгрузке (оставить введённый запрос как значение, пока данные не пришли). |
scrollToSelectedItem | boolean | — | no | Флаг, отвечающий за прокручивание до выбранного элемента |
search | { value?: string; defaultValue?: string; onChange?(value: string): void; } | undefined | — | no | Управляемое/неуправляемое состояние строки поиска (текста в поле). Позволяет потребителю читать и задавать запрос (например, для серверного поиска вместе с `autocomplete`). |
searchable | boolean | true | no | Включить поиск/ввод в поле — пользователь печатает, список фильтруется по подстроке лейбла. |
selectedOptionFormatter | ((selected: { id: ItemId; label: string; }) => string) | — | no | Кастомный форматтер лейбла выбранного значения. Применяется к каждому выбранному элементу (single — значение в поле, multiple — лейбл чипа или элемент в comma-joined). |
selection | "multiple" | "single" | — | no | Режим выбора. По умолчанию `'single'`. Режим выбора |
showClearButton | boolean | true | no | Показывать кнопку очистки значения (✕). Активна, когда есть выбранное значение и поле не disabled/readonly. |
showCopyButton | boolean | true | no | Показывать кнопку копирования значения (только при `readonly` и непустом значении). |
showHintIcon | boolean | — | no | Отображение статус-иконки у подсказки (по умолчанию `true`) |
size | "l" | "m" | "s" | — | no | Размер |
untouchableScrollbars | boolean | — | no | Отключает возможность взаимодействовать со скролбарами мышью. |
validationState | "default" | "error" | "success" | "warning" | — | no | Состояние валидации |
value | ItemId | ItemId[] | — | no | Управляемое значение Управляемые значения |
virtualized | boolean | — | no | Включить виртуализацию элементов списка. Рекомендуется при количестве элементов от 1000. |
widthStrategy | "auto" | "eq" | "gte" | 'eq' — равна ширине триггера | no | Стратегия ширины дроплиста. |