ToastUserAction

Короткое уведомление об успешно выполненном действии пользователя — copy/save/delete и т.п. Один label, без описания. Опционально — ссылка-отмена и таймер обратного отсчёта. Auto-dismiss 2s.

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

  • Подтвердить локальное действие («Скопировано», «Сохранено», «Удалено»).
  • Дать возможность отмены через action.onClick.
  • Показать short-lived loading-состояние через loading: true + потом update.*.

Когда не нужен UserAction:

  • Системные события / асинхронные результаты — используй SystemEvent.
  • Прогресс загрузки — используй Upload.

Анатомия

Appearance (default neutral)

  • neutral — нейтральный feedback (дефолт).
  • success — успех.
  • warning — предупреждение.
  • error — ошибка.

Loading (default false)

Заменяет иконку appearance на анимированный Sun-спиннер. Используется для in-progress операций с последующим toaster.userAction.update.success(id, ...).

Timer (default false)

24×24 SVG-кольцо обратного отсчёта вокруг иконки. Длительность совпадает с AUTO_CLOSE_TIME[UserAction] (2s). Pause при hover контейнера.

Установка

import { toaster, ToasterContainer, TOASTER_TYPE } from '@cloud-ru/ds-toaster'

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

Базовые appearance

Базовые appearanceSuccess / error / с ссылкой-отменой
tsx
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_USER_ACTION, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';

const CONTAINER_ID = 'demo-user-action-basic';

export function UserActionBasic() {
  const dismiss = () => toaster.userAction.dismiss({ containerId: CONTAINER_ID });

  return (
    <PortalContextProvider>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
        <Button
          appearance={APPEARANCE.Primary}
          label='Success'
          onClick={() => toaster.userAction.success({ label: 'Скопировано', containerId: CONTAINER_ID })}
        />
        <Button
          appearance={APPEARANCE.Critical}
          label='Error'
          onClick={() => toaster.userAction.error({ label: 'Не удалось', containerId: CONTAINER_ID })}
        />
        <Button
          appearance={APPEARANCE.Neutral}
          label='С отменой'
          onClick={() =>
            toaster.userAction.neutral({
              label: 'Сохранено',
              action: {
                label: 'Отменить',
                onClick: () => {
                  dismiss();
                  toaster.userAction.success({ label: 'Отменено', containerId: CONTAINER_ID });
                },
              },
              containerId: CONTAINER_ID,
            })
          }
        />
        <Button appearance={APPEARANCE.Neutral} label='Закрыть все' onClick={dismiss} />
        <ToasterContainer
          type={TOASTER_TYPE.UserAction}
          containerId={CONTAINER_ID}
          position={POSITION_USER_ACTION.BottomCenter}
          limit={2}
          autoClose={2000}
        />
      </div>
    </PortalContextProvider>
  );
}

Loading → success

Loading → successОткрываем loading-тост, через 1.5s обновляем на success
tsx
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_USER_ACTION, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';

const CONTAINER_ID = 'demo-user-action-loading';
const OPERATION_MS = 1500;

export function UserActionLoading() {
  const run = async () => {
    const id = await toaster.userAction.neutral({
      label: 'Загрузка…',
      loading: true,
      containerId: CONTAINER_ID,
    });
    setTimeout(() => {
      toaster.userAction.update.success(id, { label: 'Готово!', containerId: CONTAINER_ID });
    }, OPERATION_MS);
  };

  return (
    <PortalContextProvider>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
        <Button appearance={APPEARANCE.Primary} label='Запустить операцию' onClick={run} />
        <Button
          appearance={APPEARANCE.Neutral}
          label='Закрыть все'
          onClick={() => toaster.userAction.dismiss({ containerId: CONTAINER_ID })}
        />
        <ToasterContainer
          type={TOASTER_TYPE.UserAction}
          containerId={CONTAINER_ID}
          position={POSITION_USER_ACTION.BottomCenter}
          limit={2}
          autoClose={2000}
        />
      </div>
    </PortalContextProvider>
  );
}

С таймером обратного отсчёта

С таймером обратного отсчётаtimer=true — SVG-кольцо вокруг иконки крутится синхронно с autoClose
tsx
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_USER_ACTION, toaster, TOASTER_TYPE, ToasterContainer } from '@cloud-ru/ds-toaster';

const CONTAINER_ID = 'demo-user-action-timer';

export function UserActionTimer() {
  const dismiss = () => toaster.userAction.dismiss({ containerId: CONTAINER_ID });

  return (
    <PortalContextProvider>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
        <Button
          appearance={APPEARANCE.Primary}
          label='Тост с таймером'
          onClick={() =>
            toaster.userAction.success({
              label: 'Скопировано',
              timer: true,
              containerId: CONTAINER_ID,
            })
          }
        />
        <Button
          appearance={APPEARANCE.Neutral}
          label='С таймером и ссылкой'
          onClick={() =>
            toaster.userAction.neutral({
              label: 'Сохранено',
              timer: true,
              action: { label: 'Отменить', onClick: dismiss },
              containerId: CONTAINER_ID,
            })
          }
        />
        <Button appearance={APPEARANCE.Neutral} label='Закрыть все' onClick={dismiss} />
        <ToasterContainer
          type={TOASTER_TYPE.UserAction}
          containerId={CONTAINER_ID}
          position={POSITION_USER_ACTION.BottomCenter}
          limit={2}
          autoClose={4000}
        />
      </div>
    </PortalContextProvider>
  );
}

Положения контейнера

Положения контейнераUserAction поддерживает только top-center и bottom-center. Каждое — свой ToasterContainer.
tsx
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import { POSITION_USER_ACTION, toaster, TOASTER_TYPE, ToasterContainer, UserActionPosition } from '@cloud-ru/ds-toaster';

const POSITIONS: UserActionPosition[] = Object.values(POSITION_USER_ACTION);

const containerIdFor = (position: UserActionPosition) => `demo-user-action-position-${position}`;

export function UserActionPositions() {
  const fire = (position: UserActionPosition) =>
    toaster.userAction.success({
      label: `Тост в ${position}`,
      containerId: containerIdFor(position),
    });

  const dismissAll = () =>
    POSITIONS.forEach(position => toaster.userAction.dismiss({ containerId: containerIdFor(position) }));

  return (
    <PortalContextProvider>
      <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
        {POSITIONS.map(position => (
          <Button key={position} appearance={APPEARANCE.Primary} label={position} onClick={() => fire(position)} />
        ))}
        <Button appearance={APPEARANCE.Neutral} label='Закрыть все' onClick={dismissAll} />
        {POSITIONS.map(position => (
          <ToasterContainer
            key={position}
            type={TOASTER_TYPE.UserAction}
            containerId={containerIdFor(position)}
            position={position}
            limit={2}
            autoClose={3000}
          />
        ))}
      </div>
    </PortalContextProvider>
  );
}

Императивный API

Тосты создаются через toaster.userAction.<appearance>({...}) — один метод на каждый visual вариант. Обновление существующего тоста — toaster.userAction.update.<appearance>(id, {...}). Закрытие — toaster.userAction.dismiss(...).

import { toaster, type UserActionOptions } from '@cloud-ru/ds-toaster'

const id = await toaster.userAction.warning({
  id: 'unsaved-changes',
  containerId: 'main',
  title: 'Есть несохранённые изменения',
  description: 'Сохранить перед выходом?',
  actions: [
    { label: 'Сохранить', onClick: save },
    { label: 'Отмена', onClick: dismiss },
  ],
  autoClose: false,
  onClose: id => log('closed', id),
})

toaster.userAction.dismiss(id)

Props

Types

PropsToastUserActionProps
PropTypeDefaultRequiredDescription
actionToastUserActionAction<"button"> | ToastUserActionAction<"a">noSlot для action-кнопки (по умолчанию `<button>`, через `as='a'` — линка)
appearance"error" | "neutral" | "success" | "warning"neutralnoЦветовая схема (status в Figma)
classNamestringnoДоп. класс корневого элемента
closeToast(() => void)no
data-test-idstringno
labelstringyesТекст-уведомление
loadingbooleanfalsenoLoading-состояние с заменой иконки на Sun-спиннер
timerbooleanfalsenoПоказывать ли таймер обратного отсчёта (24×24 SVG-кольцо). Анимация = `AUTO_CLOSE_TIME[UserAction]`. Пауза при hover. Figma axis: `timer=true|false`.
toastProps{ autoClose?: number | false; }no

Unions

Types

ToastUserActionProps

Unions

Storybook

Playground — все props через controls (appearance, loading, timer, action-preset, width):

VisualMatrix · все оси

Appearance × Width, Loading & Timer, набор action-пресетов (none / labelOnly / link):

Связанные стори контейнера:

Figma

Смотри также