Dropdown

Выпадающий блок над триггером: произвольный контент (меню, фильтры, справочная информация) + встроенные состояния loading / not-found / no-data / data-error через state. Поверх PopoverPrivate — те же пропсы позиционирования, плюс готовая визуальная обработка асинхронных случаев.

Когда использовать

  • Меню действий над кнопкой (экспорт, фильтры, настройки).
  • Асинхронные подсказки и suggestions — встроенный state скрывает ручное ветвление UI.
  • Композитные виджеты: селекторы, комбобоксы, авторасшифровки.

Когда не нужен Dropdown: для одиночного подсказывающего текста используйте Tooltip, для модального выбора — Modal или Popover.

Анатомия

State

Встроенные состояния контента, позволяющие не ветвить UI вручную: loading — идёт запрос (скелетон/спиннер), no-data — у источника пусто (первая загрузка), not-found — пользователь ввёл фильтр и ничего не нашлось, data-error — запрос упал (с опцией повтора).

Высота и прокрутка

Компонент высоту не ограничивает и своей прокрутки не даёт: содержимое dropdown произвольное, поэтому предел высоты проектируется вместе с ним. Для типового случая — списка опций — есть готовый Droplist из @cloud-ru/ds-list: он сам ограничивает высоту (256 / 320 / 384px по размеру) и включает прокрутку через limitedScrollHeight.

Width strategy (default gte)

Ширина dropdown относительно триггера:

  • auto — по содержимому, триггер не учитывается.
  • eq — ровно по триггеру: width фиксируется его шириной, содержимое сжимается или обрезается.
  • gte — не уже триггера: ширина по содержимому, но min-width равна ширине триггера.

Разница между gte и eq видна, когда содержимое шире триггера: eq сожмёт список до ширины триггера, gte даст ему растянуться. Когда содержимое уже триггера, оба дают одинаковый результат — при триггере 96 и содержимом 40 auto даёт 40, eq и gte — по 96.

Значения gte в макетах нет: Figma рисует только «по содержимому» и «по триггеру». Это осознанное расширение API — промежуточный случай встречается в продукте чаще остальных, поэтому он и стоит дефолтом.

Установка

pnpm add @cloud-ru/ds-dropdown
import { Dropdown, STATE } from '@cloud-ru/ds-dropdown'

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

Базовый Dropdown

Базовый Dropdown
tsx
import { Button } from '@cloud-ru/ds-button';
import { Dropdown } from '@cloud-ru/ds-dropdown';

export function Basic() {
  return (
    <Dropdown content={<div style={{ padding: 12 }}>Контент меню</div>}>
      <Button label='Открыть' />
    </Dropdown>
  );
}

Открытое меню для визуальной сверки

Открытое меню для визуальной сверкиУправляемый режим — open
tsx
import { Button } from '@cloud-ru/ds-button';
import { Dropdown } from '@cloud-ru/ds-dropdown';

export function OpenForReview() {
  return (
    <Dropdown content={<div style={{ padding: 12 }}>Видимое содержимое</div>}>
      <Button label='Триггер' />
    </Dropdown>
  );
}

Состояние loading

Состояние loading
tsx
import { Button } from '@cloud-ru/ds-button';
import { Dropdown, STATE } from '@cloud-ru/ds-dropdown';

export function Loading() {
  return (
    <Dropdown state={{ type: STATE.Loading }} content={null}>
      <Button label='Загрузка' />
    </Dropdown>
  );
}

Состояние not-found с действием

Состояние not-found с действием
tsx
import { Button } from '@cloud-ru/ds-button';
import { Dropdown, STATE } from '@cloud-ru/ds-dropdown';

export function NotFound() {
  return (
    <Dropdown
      state={{
        type: STATE.NotFound,
        content: 'Ничего не нашли',
        actionLabel: 'Сбросить фильтры',
        onActionClick: () => {},
      }}
      content={null}
    >
      <Button label='Поиск' />
    </Dropdown>
  );
}

Props

Адаптивность

Dropdown — адаптивный компонент с переключением поверхности (surface-swap). Раскладку он берёт из AdaptiveProvider (контекст @cloud-ru/ds-adaptive); публичный API единый для обеих платформ:

  • desktop (по умолчанию) — popover над триггером с позиционированием через floating-ui.
  • mobile — контент рендерится в BottomSheet из @cloud-ru/ds-bottom-sheet (панель снизу со свайпом для закрытия).

Верстайте под desktop и поставьте один <AdaptiveProvider> в корне приложения — mobile-поверхность включается автоматически (desktop-first). Пропа layoutType у компонента нет: источник раскладки — только контекст.

Как форсировать платформу

Форс — только контекстом, не пропом:

  • Поддерево — вложенный провайдер:
    import { AdaptiveProvider } from '@cloud-ru/ds-adaptive'
    
    <AdaptiveProvider layoutType='mobile'>
      <Dropdown content={…}>…</Dropdown>
    </AdaptiveProvider>
  • Отдельный компонент — withLayoutType (module-scope, сахар над провайдером):
    import { withLayoutType } from '@cloud-ru/ds-adaptive'
    import { Dropdown } from '@cloud-ru/ds-dropdown'
    
    const MobileDropdown = withLayoutType(Dropdown, 'mobile')

Платформенные пропы

Часть пропов управляет позиционированием desktop-popover’а и на mobile молча игнорируется (у BottomSheet своё позиционирование снизу). Таблица синхронизирована с type-level JSDoc у DropdownProps.

Пропыdesktopmobile
placement, widthStrategy, offset, fallbackPlacementsиспользуетсяигнорируется
hoverDelayOpen, hoverDelayClose, closeOnEscapeKey, triggerClickByKeys, outsideClickиспользуетсяигнорируется
disableSpanWrapper, triggerClassName, triggerRef, containerиспользуетсяигнорируется
content, title, slotAfterTitle, search, footer, headerDivider, footerDividerиспользуетсяиспользуется
state, open, onOpenChange, closeOnPopstate, classNameиспользуетсяиспользуется

На mobile портал BottomSheet берётся из @cloud-ru/ds-portal-context, поэтому container не действует.

Подробнее о модели адаптивности — Adaptive.

Storybook

Figma