ToastUpload
Тост для прогресса загрузки одного или нескольких файлов. Содержит общий прогресс-бар, список файлов с per-file статусами и actions (pause/resume/cancel/retry), кнопку collapse/expand. По умолчанию не auto-dismiss’ится — закрывается явно через crossклик или вызов API.
Когда использовать
- Загрузка одного или нескольких файлов с возможностью отмены / паузы.
- Длительная операция, требующая видимости прогресса и контроля.
Когда не нужен Upload:
- Короткий ack об успешной загрузке (без прогресса) — используй UserAction с
success. - Системное уведомление об ошибке без прогресса — используй SystemEvent с
error.
Анатомия
Status
Общий статус загрузки:
loading— идёт загрузка (анимированный progress).pause— пауза.error— ошибка для всей операции.uploaded— все файлы загружены.errorUploaded— частичный результат (часть с ошибкой).
Collapsed (default false)
Сворачивает список файлов, оставляя только заголовок и общий progress. Управляется через collapsed + onCollapsed.
Files
Массив UploadItem с per-file статусом и actions. Каждый item опционально поддерживает subscribeToState для частых апдейтов прогресса без ре-рендера всего списка.
General actions
generalActions — controls для всей операции (onPause, onContinue, onRetry). Кнопка cancel отдельным prop cancelButton.
Установка
import { toaster, ToasterContainer, TOASTER_TYPE, UploadItem } from '@cloud-ru/ds-toaster'
Примеры использования
Базовый Upload
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import {
POSITION_SYSTEM_EVENT,
TOAST_UPLOAD_ITEM_STATUS,
toaster,
TOASTER_TYPE,
ToasterContainer,
UploadItem,
} from '@cloud-ru/ds-toaster';
import { useState } from 'react';
import styles from './styles.module.scss';
const CONTAINER_ID = 'demo-upload-basic';
const TOAST_ID = 'demo-upload-basic-toast';
const initialFiles: UploadItem[] = [
{
id: '1',
title: 'document.pdf',
status: TOAST_UPLOAD_ITEM_STATUS.Loading,
statusLabel: 'Загрузка...',
progress: 45,
formattedSize: '12.4 МБ',
},
{
id: '2',
title: 'image.png',
status: TOAST_UPLOAD_ITEM_STATUS.Uploaded,
statusLabel: 'Загружено',
progress: 100,
formattedSize: '2.1 МБ',
actions: {},
link: { label: 'Открыть', href: '#' },
},
];
const CLOSE_DELAY_MS = 1500;
export function UploadBasic() {
const [files, setFiles] = useState<UploadItem[]>(initialFiles);
const removeFile = (id: string) => setFiles(prev => prev.filter(f => f.id !== id));
const cancelAll = () => {
const stopped = files.map(f =>
f.status === TOAST_UPLOAD_ITEM_STATUS.Loading ? { ...f, status: TOAST_UPLOAD_ITEM_STATUS.Pause, actions: {} } : f,
);
setFiles(stopped);
toaster.upload.startOrUpdate({
id: TOAST_ID,
title: 'Загрузка остановлена',
description: 'Закроется автоматически',
status: 'pause',
progress: { current: 0, total: stopped.length },
files: stopped,
closable: true,
containerId: CONTAINER_ID,
});
setTimeout(() => {
toaster.upload.dismiss({ containerId: CONTAINER_ID });
setFiles(initialFiles);
}, CLOSE_DELAY_MS);
};
const filesWithActions: UploadItem[] = files.map(f =>
f.status === TOAST_UPLOAD_ITEM_STATUS.Loading ? { ...f, actions: { onCancel: () => removeFile(f.id ?? '') } } : f,
);
const open = () => {
const completed = filesWithActions.filter(f => f.status === TOAST_UPLOAD_ITEM_STATUS.Uploaded).length;
toaster.upload.startOrUpdate({
id: TOAST_ID,
title: 'Загрузка файлов',
description: `${completed} из ${filesWithActions.length} файлов`,
status: 'loading',
progress: { current: completed, total: filesWithActions.length },
files: filesWithActions,
closable: true,
onCancelAll: cancelAll,
containerId: CONTAINER_ID,
});
};
return (
<PortalContextProvider>
<div className={styles.uploadFrame}>
<Button appearance={APPEARANCE.Primary} label='Открыть Upload' onClick={open} />
<Button
appearance={APPEARANCE.Neutral}
label='Сбросить'
onClick={() => {
setFiles(initialFiles);
toaster.upload.dismiss({ containerId: CONTAINER_ID });
}}
/>
<ToasterContainer
type={TOASTER_TYPE.Upload}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={1}
autoClose={false}
/>
</div>
</PortalContextProvider>
);
}Прогрессивная загрузка → uploaded
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import {
POSITION_SYSTEM_EVENT,
TOAST_UPLOAD_ITEM_STATUS,
TOAST_UPLOAD_STATUS,
toaster,
TOASTER_TYPE,
ToasterContainer,
UploadItem,
} from '@cloud-ru/ds-toaster';
import { useEffect, useRef } from 'react';
import styles from './styles.module.scss';
const CONTAINER_ID = 'demo-upload-progress';
const TOAST_ID = 'demo-upload-progress-toast';
const TICK_MS = 400;
const CLOSE_DELAY_MS = 1500;
function getStatusLabel(progress: number, status: UploadItem['status']): string {
if (status === TOAST_UPLOAD_ITEM_STATUS.Uploaded) return 'Загружено';
if (status === TOAST_UPLOAD_ITEM_STATUS.Error) return 'Ошибка';
return `${progress}%`;
}
function makeFile(progress: number, status: UploadItem['status'], onCancel: () => void): UploadItem {
return {
id: '1',
title: 'video.mp4',
status,
statusLabel: getStatusLabel(progress, status),
progress,
formattedSize: '48.6 МБ',
actions: status === TOAST_UPLOAD_ITEM_STATUS.Loading ? { onCancel } : {},
...(status === TOAST_UPLOAD_ITEM_STATUS.Uploaded ? { link: { label: 'Открыть', href: '#' } } : {}),
};
}
export function UploadProgress() {
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const progressRef = useRef(0);
const stop = () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
const cancel = () => {
stop();
if (closeTimerRef.current) return;
toaster.upload.startOrUpdate({
id: TOAST_ID,
title: 'Загрузка остановлена',
description: 'Закроется автоматически',
status: TOAST_UPLOAD_STATUS.Pause,
progress: { current: progressRef.current, total: 100 },
files: [makeFile(progressRef.current, TOAST_UPLOAD_ITEM_STATUS.Pause, cancel)],
generalActions: {},
closable: true,
containerId: CONTAINER_ID,
});
closeTimerRef.current = setTimeout(() => {
closeTimerRef.current = null;
toaster.upload.dismiss({ containerId: CONTAINER_ID });
}, CLOSE_DELAY_MS);
};
useEffect(
() => () => {
if (intervalRef.current) clearInterval(intervalRef.current);
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
},
[],
);
const start = () => {
stop();
if (closeTimerRef.current) {
clearTimeout(closeTimerRef.current);
closeTimerRef.current = null;
}
progressRef.current = 0;
let progress = 0;
toaster.upload.startOrUpdate({
id: TOAST_ID,
title: 'Загрузка файла',
description: `${progress}% из 100%`,
status: TOAST_UPLOAD_STATUS.Loading,
progress: { current: progress, total: 100 },
files: [makeFile(progress, TOAST_UPLOAD_ITEM_STATUS.Loading, cancel)],
generalActions: { onPause: stop },
closable: true,
onCancelAll: cancel,
containerId: CONTAINER_ID,
});
intervalRef.current = setInterval(() => {
progress += 10;
progressRef.current = progress;
const done = progress >= 100;
toaster.upload.startOrUpdate({
id: TOAST_ID,
title: done ? 'Файл загружен' : 'Загрузка файла',
description: done ? '1 из 1 файлов' : `${progress}% из 100%`,
status: done ? TOAST_UPLOAD_STATUS.Uploaded : TOAST_UPLOAD_STATUS.Loading,
progress: { current: progress, total: 100 },
files: [
makeFile(progress, done ? TOAST_UPLOAD_ITEM_STATUS.Uploaded : TOAST_UPLOAD_ITEM_STATUS.Loading, cancel),
],
generalActions: {},
closable: true,
onCancelAll: done ? undefined : cancel,
containerId: CONTAINER_ID,
});
if (done) stop();
}, TICK_MS);
};
return (
<PortalContextProvider>
<div className={styles.uploadFrame}>
<Button appearance={APPEARANCE.Primary} label='Запустить загрузку' onClick={start} />
<Button appearance={APPEARANCE.Neutral} label='Закрыть все' onClick={cancel} />
<ToasterContainer
type={TOASTER_TYPE.Upload}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={1}
autoClose={false}
/>
</div>
</PortalContextProvider>
);
}Хуки закрытия: onCloseClick + onClose
- Лог пуст
import { APPEARANCE, Button } from '@cloud-ru/ds-button';
import { PortalContextProvider } from '@cloud-ru/ds-portal-context';
import {
POSITION_SYSTEM_EVENT,
TOAST_UPLOAD_ITEM_STATUS,
TOAST_UPLOAD_STATUS,
toaster,
TOASTER_TYPE,
ToasterContainer,
UploadItem,
} from '@cloud-ru/ds-toaster';
import { useState } from 'react';
import styles from './styles.module.scss';
const CONTAINER_ID = 'demo-upload-close-hooks';
const TOAST_ID = 'demo-upload-close-hooks-toast';
const file: UploadItem = {
id: '1',
title: 'archive.zip',
status: TOAST_UPLOAD_ITEM_STATUS.Loading,
statusLabel: '40%',
progress: 40,
formattedSize: '120 МБ',
};
export function UploadCloseHooks() {
const [log, setLog] = useState<string[]>([]);
const push = (line: string) => setLog(prev => [line, ...prev].slice(0, 4));
const open = () => {
toaster.upload.startOrUpdate({
id: TOAST_ID,
title: 'Загрузка файла',
description: '40% из 100%',
status: TOAST_UPLOAD_STATUS.Loading,
progress: { current: 40, total: 100 },
files: [file],
closable: true,
containerId: CONTAINER_ID,
onCloseClick: (_e, close) => {
if (window.confirm('Закрыть тост загрузки?')) {
push('onCloseClick → close()');
close?.();
} else {
push('onCloseClick → отменено');
}
},
onClose: id => push(`onClose(${String(id)})`),
});
};
return (
<PortalContextProvider>
<div className={styles.uploadFrame}>
<Button appearance={APPEARANCE.Primary} label='Открыть Upload' onClick={open} />
<Button appearance={APPEARANCE.Neutral} label='Очистить лог' onClick={() => setLog([])} />
<ul className={styles.eventLog} aria-label='Лог событий'>
{log.length === 0 ? <li>Лог пуст</li> : log.map((line, i) => <li key={i}>{line}</li>)}
</ul>
<ToasterContainer
type={TOASTER_TYPE.Upload}
containerId={CONTAINER_ID}
position={POSITION_SYSTEM_EVENT.BottomRight}
limit={1}
autoClose={false}
/>
</div>
</PortalContextProvider>
);
}Императивный API
Тосты создаются через toaster.upload.startOrUpdate({...}). Один и тот же id обновляет существующий тост — отдельный update-метод не нужен. Закрытие — toaster.upload.dismiss(...).
import { toaster, type UploadOptions } from '@cloud-ru/ds-toaster'
const id = await toaster.upload.startOrUpdate({
id: 'main-upload',
containerId: 'main',
title: 'Загрузка файлов',
description: '40% из 100%',
status: 'loading',
progress: { current: 40, total: 100 },
files: [
{ id: '1', title: 'doc.pdf', status: 'loading', statusLabel: '40%', progress: 40, formattedSize: '12 МБ' },
],
onCancelAll: () => abortAllUploads(),
onCloseClick: (_e, close) => confirm('Закрыть?') && close?.(),
onClose: id => log('closed', id),
})
toaster.upload.dismiss({ containerId: 'main' })
Props
Types
ToastUploadProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
className | string | — | no | CSS-класс контейнера |
closable | boolean | true | no | Показывать кнопку закрытия тостера. По умолчанию `true`. |
closeToast | (() => void) | — | no | |
collapsed | boolean | — | no | Тостер свернут/развернут |
data-test-id | string | — | no | |
description | string | — | yes | Описание статуса загрузки |
files | UploadItem[] | — | yes | Загружаемые элементы |
generalActions | Omit<UploadActions, "onCancel"> | — | no | Экшены для управления общей загрузкой |
onCancelAll | ((e: MouseEvent<HTMLButtonElement, MouseEvent>) => void) | — | no | Колбэк кнопки отмены всей загрузки. Кнопка рендерится только при наличии колбэка; подпись — из `@cloud-ru/ds-locale` (`ToastUpload.cancelAll`). |
onCloseClick | ((e: MouseEvent<HTMLButtonElement, MouseEvent>, close?: (() => void)) => void) | — | no | Закрыть тостер |
onCollapsed | ((collapsed: boolean) => void) | — | no | Развернуть/свернуть тостер |
progress | { current: number; total: number; } | — | yes | Общий прогресс загрузки |
status | "error" | "errorUploaded" | "loading" | "pause" | "uploaded" | — | yes | Общий статус загрузки |
title | string | — | no | Заголовок тостера |
toastProps | { autoClose?: number | false; } | — | no |
Unions
Types
ToastUploadProps
UploadActions
UploadItem
Unions
ToastUploadStatus
Storybook
Playground — все props через controls (status, progress, generalActions, collapsed, файлы):
VisualMatrix · статусы × состояния
Status (loading / paused / success / errorUploaded), collapsed/expanded, разные item-state’ы в списке файлов:
Связанные стори контейнера:
- UpdateFlow — реалистичный сценарий: один тост проходит
loading → success → errorчерезtoaster.upload.startOrUpdate:components-toaster-toaster--interaction-test. - Stacking — общий контейнер
Upload + SystemEvent(Upload-карточка с одной стороны, стек SystemEvent с другой, кнопки между):components-toaster-toaster--visual-matrix. - Mobile — Upload внутри узкого
toasterParentсwidth: full:components-toaster-toaster--composition.
Figma
Смотри также
- Toaster (root) — обзор пакета.