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'

Примеры использования

Выбор одного значения

Выбор одного значенияControlled FieldSelect с single selection, label и placeholder.
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}
    />
  );
}

Множественный выбор с чипами

Множественный выбор с чипамиselection=multiple + chips: выбранные значения отображаются как удаляемые чипы.
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}
    />
  );
}

Поиск по списку

Поиск по спискуsearchable + enableFuzzySearch: ввод фильтрует список по лейблу.
Нечёткий поиск: «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 с копированием

Readonly с копированиемReadonly FieldSelect показывает кнопку копирования лейбла в буфер.
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' />;
}

Защита отключённых чипов

Защита отключённых чиповАйтем с disabled остаётся чипом без кнопки удаления и не сбрасывается кнопкой очистки.
ЧтениеЗапись
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)autocomplete + search: фильтрацию выполняет потребитель, строка поиска управляется снаружи.
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

Создание значения по EnteraddOptionByEnter: введённый текст по 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}
    />
  );
}

Закреплённые опции

Закреплённые опцииpinTop/pinBottom закрепляют опции над и под основным списком — они не участвуют в поиске и всегда видны.
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}
    />
  );
}

Опции с описанием

Опции с описаниемАйтемы списка поддерживают caption и description (формат @cloud-ru/ds-list ItemContent); поиск матчит по всем трём полям.
Поиск ищет по названию, характеристикам и описанию
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)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

PropsFieldSelectProps
PropTypeDefaultRequiredDescription
addOptionByEnterbooleanfalsenoЗафиксировать введённый текст как новый выбор по `Enter` (создание опции «на лету»).
autoFocusbooleannoАвтофокус input при монтировании. На mobile выключается адаптивно (см. `layoutPresets`)
autocompletebooleanfalsenoНе фильтровать список на клиенте — фильтрацию обеспечивает потребитель (серверный поиск). Введённый текст уходит в `search.onChange`, список берётся из `items` как есть.
backgroundbooleantruenoФон поля (acrylic)
captionstringnoВторичная подпись справа
chipsbooleantruenoОтображать выбранные значения как чипы (`@cloud-ru/ds-tag`) внутри поля. Если `false`, показывает строку из `formatSelected` либо comma-joined.
classNamestringnoCSS-класс CSS-класс корня `FieldDecorator`
closeDroplistOnItemClickbooleantrue falsenoЗакрывать дроплист после клика на айтем.
closeOnPopstatebooleannoЗакрывать ли поповер при переходе по истории браузера
data-test-idstringnoТестовый id корня
dataErrorbooleannoФлаг «ошибка загрузки данных» — при `true` дроплист рендерит `errorDataState` вместо списка (для асинхронной подгрузки с провалившимся запросом).
dataFilteredbooleannoФлаг «список отфильтрован» — при `true` и пустом результате дроплист рендерит `noResultsState`. По умолчанию выводится из строки поиска (`searchable` + ввод).
defaultValueItemId | ItemId[]noНеуправляемое значение по умолчанию Неуправляемые значения по умолчанию
disabledbooleannoПоле выключено Деактивировано
enableFuzzySearchbooleantruenoВключить нечёткий поиск: символы запроса должны встречаться в лейбле в том же порядке (например, `lge` найдёт `Large`). Если `false` — простой substring-match.
errorstringnoОшибка (приоритетнее `hint`; форсит `validationState=error`)
errorDataStateEmptyStatePropsnoЭкран при ошибке запроса
fieldClassNamestringnoCSS-класс оболочки поля
footerReactNode ;noКастомизируемый элемент в конце списка
footerActiveElementsRefsRefObject<HTMLElement>[]noСписок ссылок на кастомные элементы, помещенные в специальную секцию внизу списка
formatSelected((selected: { id: ItemId; label: string; }[]) => string)— список лейблов через `, `noФорматтер строки выбранных значений (используется, если `chips=false`).
hintstringnoПодсказка
iconBeforeReactNodenoИконка перед текстом
idstringnoHTML-атрибут `id` для input (и `for` у label)
innerRefRef<HTMLDivElement>noRef на корневой DOM-элемент
itemsItem[]yesСписок айтемов дроплиста (формат `@cloud-ru/ds-list`)
labelstringnoЗаголовок
labelForstringnoHTML-атрибут `for` для `<label>`
labelTooltipQuestionTooltipPropsnoПодсказка (question-tooltip) у заголовка
layoutPresetsPartial<Record<LayoutType, Partial<{ autoFocus: boolean; }>>>noПереопределение адаптивных дефолтов по раскладке. Участвует `autoFocus`: на mobile он выключен (открывает клавиатуру без действия). Вернуть на mobile — `layoutPresets={{ mobile: { autoFocus: true } }}`.
lengthFieldLengthnoСчётчик длины `current/max`
limitedScrollHeightbooleannoОграничить максимальную высоту скролл-контейнера в зависимости от `size`
loadingbooleannoФлаг, отвечающий за состояние загрузки списка
namestringnoHTML-атрибут `name` для input
noDataStateEmptyStatePropsnoЭкран при отсутствии данных
noResultsStateEmptyStatePropsnoЭкран при отсутствии результатов поиска или фильтров
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Колбек смены открытия
openbooleannoУправляемое открытие дроплиста
pinBottomItem[]noПресет-айтемы снизу (формат `@cloud-ru/ds-list`)
pinTopItem[]noПресет-айтемы сверху (формат `@cloud-ru/ds-list`)
placeholderstringnoPlaceholder в триггере, когда нет выбранного значения
placement"bottom" | "bottom-end" | "bottom-start" | "left" | "left-end" | "left-start" | "right" | "right-end" | "right-start" | "top" | "top-end" | "top-start"noPlacement дроплиста
postfixReactNodenoПостфикс — текст или нода после значения (Figma `postfix`)
prefixReactNodenoПрефикс — текст или нода перед значением (Figma `prefix`)
readonlybooleannoТолько для чтения Read-only режим
removeByBackspacebooleantruenoУдалять последний чип по `Backspace`, когда строка ввода пустая. Работает только при `chips=true` и `searchable=true`.
requiredbooleannoПоказать знак обязательности `*`
resetSearchOnOptionSelectionbooleantruenoСбрасывать строку поиска к выбранному значению после выбора. `false` нужен при асинхронной подгрузке (оставить введённый запрос как значение, пока данные не пришли).
scrollToSelectedItembooleannoФлаг, отвечающий за прокручивание до выбранного элемента
search{ value?: string; defaultValue?: string; onChange?(value: string): void; } | undefinednoУправляемое/неуправляемое состояние строки поиска (текста в поле). Позволяет потребителю читать и задавать запрос (например, для серверного поиска вместе с `autocomplete`).
searchablebooleantruenoВключить поиск/ввод в поле — пользователь печатает, список фильтруется по подстроке лейбла.
selectedOptionFormatter((selected: { id: ItemId; label: string; }) => string)noКастомный форматтер лейбла выбранного значения. Применяется к каждому выбранному элементу (single — значение в поле, multiple — лейбл чипа или элемент в comma-joined).
selection"multiple" | "single"noРежим выбора. По умолчанию `'single'`. Режим выбора
showClearButtonbooleantruenoПоказывать кнопку очистки значения (✕). Активна, когда есть выбранное значение и поле не disabled/readonly.
showCopyButtonbooleantruenoПоказывать кнопку копирования значения (только при `readonly` и непустом значении).
showHintIconbooleannoОтображение статус-иконки у подсказки (по умолчанию `true`)
size"l" | "m" | "s"noРазмер
untouchableScrollbarsbooleannoОтключает возможность взаимодействовать со скролбарами мышью.
validationState"default" | "error" | "success" | "warning"noСостояние валидации
valueItemId | ItemId[]noУправляемое значение Управляемые значения
virtualizedbooleannoВключить виртуализацию элементов списка. Рекомендуется при количестве элементов от 1000.
widthStrategy"auto" | "eq" | "gte"'eq' — равна ширине триггераnoСтратегия ширины дроплиста.

Types

FieldSelectProps

Storybook

Figma