GrFileUpload
Берут, когда файл уходит на сервер сразу.
Обзор
Когда брать
- файл уходит на сервер сразу —
actionшлёт multipart сам,requestподключает свой загрузчик; - файлов много —
concurrencyограничивает параллельные запросы,limit— их число; - загрузка длинная — прогресс на файл,
hideProgressOnSuccessубирает полосу после успеха; - нужна зона перетаскивания — drag&drop из системы плюс выбор кнопкой и с клавиатуры;
- вид полностью свой — слот отдаёт зону потребителю, оставляя ему всю механику.
Когда взять другое
| Нужно | Берите |
|---|---|
| Файл — значение формы и уезжает с ней | GrFormFile |
| Изображение нужно рассмотреть | GrImageViewer |
| Нужен только индикатор процесса | GrProgressBar |
| Ошибка ответа сервера | GrResponseErrorBanner |
Playground контролы выводятся из API, а не пишутся руками
Загружается…
0 изменено· состояние — в адресе
<GrFileUpload>Текст</GrFileUpload>Примеры 3 у каждого — живое демо и его код
Импорт банковской выписки
Выписка отправляется в момент выбора:
request— собственный вызов приложения, он сообщает прогресс черезctx.onProgress, слушаетctx.signal, а его ответ типизирует событиеsuccess.acceptфильтрует системный диалог и проверяет перетащенный файл;maxFileSizeдобавляет предел. Ошибки компонент отдаёт событием, а приложение формулирует их для поля поFileValidationError.code.Import transactionsBusiness account •••• 4821 Drop a statement here or choose a fileCSV or OFX export from your bank, up to 10 MBДокументы поставщика, по запросу на файл
upload-mode="per-file"даёт каждой строке свой статус, процент, «Отмену» и «Повторить загрузку»: когда хранилище не отвечает, переотправляется один документ, а не весь набор.concurrencyдержит в полёте два запроса,limitостанавливает седьмой файл до отправки, аpreviewрисует миниатюры сфотографированных документов.Supplier onboarding · Northwind TradersRegistration certificate, VAT certificate, bank letter, ID of the signatory Drop the documents here or choose themPDF or photos, up to 20 MB each, six at mostСтрока логотипа вместо зоны перетаскивания
Слот по умолчанию заменяет зону целиком и возвращает всё её состояние: выбранные
files,stateзагрузки с фазой и процентом,openDialogиremoveFile. Строка сразу рисует файл и отзывает его адрес при смене; перетащить файл на строку по-прежнему можно.Workspace logo NTInitials are shown until a logo is setPNG or SVG, square, up to 1 MB
API порождается из пакета · разойтись не может
Props
| Prop | Type | по умолчанию | Описание |
|---|---|---|---|
modelValuev-model | File[] | undefined | undefined | Набор файлов — тот, что показан в списке и уйдёт в `retry`. Проп **необязателен**: без него компонент держит набор сам. Передан — набор следует за ним, и потребитель может очистить или подменить список снаружи. |
action | string | undefined | undefined | Адрес для POST `multipart/form-data` встроенным загрузчиком. Нужен, когда нет `request`. |
request | GrFileUploadRequest<TResponse> | undefined | undefined | Свой загрузчик (axios, fetch) вместо `action`: получает файлы и `signal`, `extraData`, `onProgress`. Сильнее `action`. |
name | string | undefined | "file" | Имя поля файла в `FormData` встроенного загрузчика. По умолчанию `file`. |
multiple | boolean | undefined | false | Выбор нескольких файлов за раз; без него `limit` не действует. По умолчанию `false`. |
limit | number | undefined | undefined | Предел файлов в одном выборе при `multiple`: сверх него — `exceed` и `error`, загрузки нет. |
beforeUpload | ((file: File) => boolean | Promise<unknown>) | undefined | undefined | Guard перед отправкой. Остаётся пропом, а не эмитом, осознанно: эмит не возвращает значения, а этот колбэк обязан ответить «пускать или нет». Уведомления — `exceed`, `success`, `error`, `progress` — эмиты. |
validators | FileValidator[] | undefined | undefined | Проверки набора до отправки, после `accept`: размер, тип, число. Нарушения приходят в `error` одним `FileValidationError`, и загрузка не стартует. |
accept | string | undefined | undefined | W3C `accept` для `<input type="file">` — и sugar к `acceptValidator(...)`. Только атрибута мало: он фильтрует системный диалог, но не drag&drop — перетащить можно что угодно. Поэтому то же значение уходит и в валидаторы, как в `GrFormFile`. |
capture | "user" | "environment" | undefined | undefined | `capture` для мобильной камеры/микрофона. |
directory | boolean | undefined | false | Выбор каталога целиком (`webkitdirectory`). Поддержка — Chromium и Safari. |
disabled | boolean | undefined | false | Контрол недоступен: диалог и перетаскивание не работают. Включается и от `GrFormField`. |
readonly | boolean | undefined | false | Только для чтения: значение видно и уходит в форму, но не редактируется. |
invalid | boolean | undefined | false | Визуальное и ARIA-состояние ошибки. |
required | boolean | undefined | false | Обязательное поле (`aria-required`). |
ariaLabel | string | undefined | undefined | Доступное имя вне `GrFormField`. |
headers | Record<string, string> | undefined | undefined | Заголовки запроса встроенного загрузчика — например, токен. С `request` не используются. |
withCredentials | boolean | undefined | false | Слать cookie на другой домен (`xhr.withCredentials`) у встроенного загрузчика. |
showFileList | boolean | undefined | false | Список выбранных файлов под зоной: имя, размер, удаление, в `per-file` — статус и повтор. По умолчанию `false`. |
uploadExtraData | ((files: File[]) => GrFileUploadExtraData | undefined) | undefined | undefined | Дополнительные поля запроса, посчитанные по набору: ложатся в `FormData` рядом с файлами, у `request` приходят в `ctx.extraData`. Исключение из колбэка уходит в `error`. |
placeholder | string | undefined | undefined | i18n: надпись-подсказка в дефолтном UI. |
showProgress | boolean | undefined | true | Показывать дефолтный прогресс-бар (если не используется слот `progress`). |
progressTone |
| "primary" | Цветовой тон полосы прогресса в фазе `uploading`. |
progressLabel | string | undefined | undefined | aria-label для прогресс-бара. |
hideProgressOnSuccess | number | undefined | 800 | Через сколько мс после `success` скрыть прогресс-бар. `0` — не скрывать. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | Размер дефолтного UI: поля дроп-зоны, плитка иконки, кегль подписей. |
uploadMode | GrFileUploadMode | undefined | "batch" | Как уходит набор файлов: - `batch` (по умолчанию) — весь набор одним запросом. Про отдельный файл сказать нечего, поэтому и статуса у него нет; - `per-file` — на каждый файл свой запрос (`request` зовётся с массивом из одного файла, контракт не меняется). Появляются статус и процент строки, `retryFile` и `abortFile`. |
concurrency | number | undefined | 3 | Сколько файлов грузится одновременно в режиме `per-file`. |
preview | boolean | undefined | false | Миниатюры для `image/*` в списке файлов. |
Слоты
| Slot | Type | Описание |
|---|---|---|
default |
| Полностью своя зона загрузки: контрол отдаёт наружу всё своё состояние. |
label | any | Заголовок зоны вместо `placeholder`. |
tip | any | Подпись под заголовком: ограничения по типу и размеру. |
progress |
| Свой индикатор прогресса вместо встроенного. |
События
| Event | Сигнатура | Описание |
|---|---|---|
update:modelValue | (e: "update:modelValue", files: File[]): void | Набор файлов сменился: новый выбор или удаление. Не путать с `change`. |
exceed | (e: "exceed", files: File[], limit: number): void | Выбрано больше файлов, чем разрешает `limit`. Загрузка не стартует. |
success | (e: "success", payload: TResponse, file?: File | undefined): void | `file` приходит только в режиме `per-file`: в батче отчитываться нечем. |
error | (e: "error", error: unknown, file?: File | undefined): void | Набор не ушёл или отправка упала: `limit`, валидаторы, `beforeUpload`, сеть, ответ сервера, отмена. `file` — только в `per-file`. |
progress |
| Прогресс отправки в процентах, от `0` до `100`. `file` — только в `per-file`. |
change | (e: "change", files: File[]): void | Набор отправлен: в `batch` — после успеха, в `per-file` — когда отработала вся очередь. Не путать с `update:modelValue`. |
stateChange | (e: "stateChange", state: GrUploadState): void | Сменилась сводная фаза загрузки — `idle`, `uploading`, `success`, `error` — с процентом. |
focus | (e: "focus", event: FocusEvent): void | Фокус вошёл в зону. Переходы между полем и кнопками списка событием не считаются. |
blur | (e: "blur", event: FocusEvent): void | Фокус ушёл из зоны целиком. |
Типы
Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.
import type { GrFileUploadMode, GrFileUploadExtraDataValue, GrFileUploadExtraData } from '@feugene/granularity/components/GrFileUpload'Файлdist/types/components/GrFileUpload/GrFileUpload.vue.d.ts
type GrFileUploadMode = 'batch' | 'per-file';type GrFileUploadExtraDataValue = string | Blob;type GrFileUploadExtraData = Record<string, GrFileUploadExtraDataValue | GrFileUploadExtraDataValue[]>;type GrFileUploadRequestCtx = {
signal: AbortSignal;
extraData?: GrFileUploadExtraData;
/**
* Колбэк прогресса аплоада. Пользовательский `request` (например, axios)
* должен вызывать его из `onUploadProgress`, чтобы `GrFileUpload` отображал
* прогресс через слот `progress` / дефолтный `GrProgressBar`.
*/
onProgress?: (info: GrUploadProgressInfo) => void;type GrFileUploadRequest<TResponse = unknown> = (files: File[], ctx: GrFileUploadRequestCtx) => Promise<TResponse>;
/**
* Пропы `GrFileUpload`.
*
* Либо `action` (URL для POST multipart/form-data), либо `request` — кастомный
* загрузчик (например, через axios). Если переданы оба — приоритет у `request`,
* а `action` не используется; в dev-сборке об этом предупреждает консоль.
*
* `placeholder` — надпись в дефолтном UI-варианте (без слота default).
*/
export interface GrFileUploadProps<TResponse = unknown> {Пропы GrFileUpload.
Либо action (URL для POST multipart/form-data), либо request — кастомный
загрузчик (например, через axios). Если переданы оба — приоритет у request,
а action не используется; в dev-сборке об этом предупреждает консоль.
placeholder — надпись в дефолтном UI-варианте (без слота default).
interface GrFileUploadProps<TResponse = unknown> {
/**
* Набор файлов — тот, что показан в списке и уйдёт в `retry`.
*
* Проп **необязателен**: без него компонент держит набор сам. Передан —
* набор следует за ним, и потребитель может очистить или подменить список
* снаружи.
*/
modelValue?: File[];
/** Адрес для POST `multipart/form-data` встроенным загрузчиком. Нужен, когда нет `request`. */
action?: string;
/**
* Свой загрузчик (axios, fetch) вместо `action`: получает файлы и `signal`, `extraData`,
* `onProgress`. Сильнее `action`.
*/
request?: GrFileUploadRequest<TResponse>;
/** Имя поля файла в `FormData` встроенного загрузчика. По умолчанию `file`. */
name?: string;
/** Выбор нескольких файлов за раз; без него `limit` не действует. По умолчанию `false`. */
multiple?: boolean;
/** Предел файлов в одном выборе при `multiple`: сверх него — `exceed` и `error`, загрузки нет. */
limit?: number;
/**
* Guard перед отправкой. Остаётся пропом, а не эмитом, осознанно: эмит не
* возвращает значения, а этот колбэк обязан ответить «пускать или нет».
* Уведомления — `exceed`, `success`, `error`, `progress` — эмиты.
*/
beforeUpload?: (file: File) => boolean | Promise<unknown>;interface GrFileUploadEmits<TResponse = unknown> {
/** Набор файлов сменился: новый выбор или удаление. Не путать с `change`. */
(e: 'update:modelValue', files: File[]): void;
/** Выбрано больше файлов, чем разрешает `limit`. Загрузка не стартует. */
(e: 'exceed', files: File[], limit: number): void;
/** `file` приходит только в режиме `per-file`: в батче отчитываться нечем. */
(e: 'success', payload: TResponse, file?: File): void;
/**
* Набор не ушёл или отправка упала: `limit`, валидаторы, `beforeUpload`, сеть, ответ сервера,
* отмена. `file` — только в `per-file`.
*/
(e: 'error', error: unknown, file?: File): void;
/** Прогресс отправки в процентах, от `0` до `100`. `file` — только в `per-file`. */
(e: 'progress', percent: number, info?: GrUploadProgressInfo, file?: File): void;
/**
* Набор отправлен: в `batch` — после успеха, в `per-file` — когда отработала вся очередь.
* Не путать с `update:modelValue`.
*/
(e: 'change', files: File[]): void;
/** Сменилась сводная фаза загрузки — `idle`, `uploading`, `success`, `error` — с процентом. */
(e: 'stateChange', state: GrUploadState): void;
/** Фокус вошёл в зону. Переходы между полем и кнопками списка событием не считаются. */
(e: 'focus', event: FocusEvent): void;
/** Фокус ушёл из зоны целиком. */
(e: 'blur', event: FocusEvent): void;
}Файлdist/types/components/GrFileUpload/fileEntry.d.ts
Состояние одного файла в пофайловом режиме (uploadMode="per-file").
Батчевый режим по-прежнему знает только сводное GrUploadState: весь набор
уходит одним запросом, и говорить про отдельный файл там нечем.
type GrFileUploadStatus = 'pending' | 'uploading' | 'success' | 'error';interface GrFileUploadEntry {
file: File;
status: GrFileUploadStatus;
/** Процент `0..100`. У `pending` — 0, у `success` — 100. */
percent: number;
/** Причина ошибки, если `status === 'error'`. */
error?: unknown;
}Файлdist/types/components/GrFileUpload/config.d.ts
const grFileUploadConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFileUpload">;Файлdist/types/components/GrFileUpload/defaults.d.ts
Пропы GrFileUpload, настраиваемые глобально через componentDefaults.
interface GrFileUploadConfigurableProps {
size: GrComponentSize;
}Файлdist/types/components/GrFileUpload/grFileUploadStyles.d.ts
type GrFileUploadSize = GrComponentSize;Файлdist/types/components/GrFileUpload/safelist.d.ts
const grFileUploadSafelist: string[];Файлdist/types/components/GrFileUpload/uploadViaXhr.d.ts
interface GrUploadProgressInfo {
/** Процент `0..100`. Для запросов без `lengthComputable` значение `0` и `indeterminate = true`. */
percent: number;
loaded: number;
total: number;
indeterminate: boolean;
}class GrUploadAbortError extends Error {
constructor(message?: string);
}class GrUploadHttpError extends Error {
status: number;
payload: unknown;
constructor(status: number, payload: unknown);
}Файлdist/types/components/GrFileUpload/uploadState.d.ts
Состояние процесса загрузки в GrFileUpload.
Используется как для внутреннего управления UI, так и пробрасывается наружу
через scoped-слоты (progress, default) и defineExpose.
type GrUploadPhase = 'idle' | 'uploading' | 'success' | 'error';interface GrUploadStateIdle {
phase: 'idle';
percent: 0;
indeterminate: false;
loaded: 0;
total: 0;
}interface GrUploadStateUploading {
phase: 'uploading';
percent: number;
indeterminate: boolean;
loaded: number;
total: number;
}interface GrUploadStateSuccess {
phase: 'success';
percent: 100;
indeterminate: false;
loaded: number;
total: number;
}interface GrUploadStateError {
phase: 'error';
percent: number;
indeterminate: false;
loaded: number;
total: number;
error: unknown;
}type GrUploadState = GrUploadStateIdle | GrUploadStateUploading | GrUploadStateSuccess | GrUploadStateError;Файлdist/types/fileValidation/types.d.ts
Откуда пришёл набор: выбор в диалоге, перетаскивание или проверка формы перед
отправкой (правило file у GrForm). Последний случай отличают не ради
симметрии: дорогой валидатор вправе работать на submit и молчать на каждом
выборе файла.
type FileValidatorSource = 'input' | 'drop' | 'form';type FileValidator = (input: FileValidatorInput) => FileValidationIssue[] | Promise<FileValidationIssue[]>;Файлdist/types/fileValidation/FileValidationError.d.ts
class FileValidationError extends Error {
code: FileValidationIssueCode;
file?: File;
issues: FileValidationIssue[];
constructor(issues: FileValidationIssue[], files?: File[], message?: string);
}Файлdist/types/components/GrFileUpload/index.d.ts
type GrFileUploadInstance = ComponentExposed<typeof GrFileUploadComponent>;Доступность
Паттерн APG
—
| Клавиша | Действие |
|---|---|
| — | остановка Tab — сам <input type="file"> (визуально скрыт, фокус показывает обёртка через focus-within); зона сброса в таб-порядок не входит. Кнопки строки файла — отмена, повтор, удаление — идут дальше по обходу |
Полный контракт пакета: docs/keyboard.md ↗
Использование из прозы библиотеки
accept фильтрует диалог, валидатор — перетаскивание
accept уходит и на <input type="file">, и первым валидатором в цепочку.
Только атрибута мало: он ограничивает системный диалог, но перетащить в зону
можно что угодно — без валидатора лишний файл дошёл бы до сервера.
<GrFileUpload accept="image/*,.pdf" :request="upload" multiple show-file-list />Рядом — capture (камера на мобильном) и directory (webkitdirectory,
Chromium и Safari).
Fallthrough-атрибуты компонент не перекладывает на input: class и style
принадлежат зоне, и inheritAttrs: false увёл бы их с неё. Атрибуты самого
input объявлены пропами явно.
Набор файлов: убрать и повторить
showFileList показывает выбранные файлы; у каждого есть кнопка удаления, а
retry() повторяет загрузку текущего набора — после ошибки выбирать файлы
заново не нужно.
const uploader = ref<GrFileUploadInstance>()
uploader.value?.retry()
uploader.value?.removeFile(file) // удаление обрывает идущую загрузку: она была про прежний набор
uploader.value?.abort()Удаление последнего файла возвращает состояние в idle: показывать ошибку от
набора, которого больше нет, не о чем.
action или request, но не оба
Куда уходят файлы, задаёт ровно один из двух пропов: action — URL для POST
multipart, request — свой загрузчик. Передашь оба — отправит request, а
action не участвует ни в чём.
Требование «ровно один» типом не выражается, и это ограничение Vue, а не
недосмотр: defineProps принимает объектный тип или интерфейс, но не
discriminated union — ключи схлопываются в плоский объект, и vue-tsc пропускает
и «оба сразу», и «ни одного». Поэтому обе ошибки ловятся в dev-сборке
предупреждением в консоли: без него первая всплывала бы на первом выборе файла,
а вторая не всплывала бы вовсе — молча проигнорированный action по поведению
неотличим от учтённого.
Режимы загрузки
uploadMode решает, чем именно является набор файлов:
batch(по умолчанию) — весь набор уходит одним запросом (request(files, ctx)). Состояние одно на набор: про отдельный файл сказать нечего, и статуса у него нет;per-file— на каждый файл свой запрос.requestпри этом зовётся с массивом из одного файла: контракт не меняется, и загрузчик потребителя (axios и прочие) продолжает работать без правок.
В пофайловом режиме у строки появляются статус (pending/uploading/
success/error) и процент, а у компонента — retryFile(file) и
abortFile(file):
<GrFileUpload
upload-mode="per-file"
:concurrency="3"
:request="upload"
multiple
show-file-list
/>concurrency (по умолчанию 3) ограничивает число одновременных запросов:
«пофайлово» без него означало бы «все сразу», и сотня файлов открыла бы сотню
соединений.
Сводное состояние считается по худшему исходу: грузимся, пока грузится хоть один; ошибка, если после завершения есть хоть одна; успех — когда успешны все. Процент — среднее по файлам: взвешенная по байтам полоса дёргалась бы назад, когда большой файл стартует после маленького.
Отмена одного файла — не ошибка: строка возвращается в очередь, и её можно повторить или убрать.
Пока файл едет, у его строки одна кнопка — «остановить» (значок
circle-stop, имя «Cancel upload of …»), а «убрать» появляется, когда загрузка
остановлена или закончилась: два одинаковых крестика рядом не различить. Строка
списка занимает всю ширину колонки: имя слева и усекается многоточием (полное
— в title), статус и кнопки стоят у общего правого края — и в узком
контейнере длинное имя их не выталкивает.
Эмиты success, error и progress получают необязательный хвостовой
аргумент file — он есть только в пофайловом режиме.
Прогресс не исчезает мгновенно
Полоса на GrProgressBar показывается сама, пока идёт
отправка, и после успеха держится ещё hideProgressOnSuccess миллисекунд —
по умолчанию 800. Задержка не косметическая: полоса, пропадающая в тот же кадр,
в котором дошла до конца, читается как «загрузка сорвалась», а не «готово».
0 оставляет полосу навсегда — это режим для своего UI, который убирает её сам.
showProgress: false выключает дефолтную полосу целиком: она не нужна, когда
прогресс рисует слот #progress или когда файлы мелкие и полоса успевает
только мигнуть. progressTone красит её в фазе отправки, progressLabel даёт
имя для скринридера.
Ответ сервера
Компонент дженерик по ответу: TResponse выводится из request и типизирует
payload события success — any из публичной сигнатуры ушёл.
async function upload(files: File[]): Promise<UploadedFile> { /* … */ }
function onUploaded(file: UploadedFile) { /* payload типизирован */ }<GrFileUpload :request="upload" @success="onUploaded" />Параметр типа задаётся именно так, через request: в шаблоне SFC синтаксиса
<GrFileUpload<UploadedFile>> не существует — это TSX, а не Vue-шаблон.
Ветка action типом ответа не управляет: что вернёт сервер, знает только
потребитель.
Превью
preview рисует миниатюру для файлов image/* в списке. Ссылка
(URL.createObjectURL) отзывается при удалении файла, смене набора и
размонтировании: без этого blob висит в памяти вкладки до перезагрузки.
Миниатюра декоративна (alt="") — имя файла рядом уже есть, и дублировать его
диктору незачем.
Контролируемый набор
<GrFileUpload v-model="files" :request="upload" show-file-list />modelValue необязателен: без него компонент держит набор сам. Передан —
набор следует за пропом, и его можно подменить или очистить снаружи, например
после того как форма отправлена.
update:modelValue эмитится, когда меняется сам набор: новый выбор или
удаление файла. Не путать с change — тот значит «загрузка завершилась», и это
другой момент.
Разница с GrFormFile осталась в назначении, а не в наличии модели:
GrFileUpload про отправку (action/request, прогресс, повтор, пофайловые
статусы), GrFormFile — про поле формы.
action и request — граница другого рода: заданы оба, работает request; не
задан ни один — dev-сборка предупреждает при монтировании, а не на первом выборе
файла. Выразить требование типом (discriminated union) в SFC нельзя:
defineProps принимает только объектный тип или интерфейс.
Guard и уведомления
beforeUpload — проп-колбэк: он обязан вернуть «пускать или нет», а эмит
значения не возвращает. Всё остальное — эмиты: exceed (файлов больше limit),
progress, success, error, change, stateChange.
:on-exceed="fn" продолжает работать: у Vue эмит и приходит пропом-слушателем.
Гонки и хвосты
- Два быстрых выбора подряд идут внахлёст (валидаторы асинхронны). Актуален всегда последний: у запуска есть номер, и отставший тихо сходит с дистанции, не обрывая загрузку соседа.
- При размонтировании компонент абортит активный XHR и снимает таймер скрытия успеха — иначе загрузка продолжалась бы, а таймер дёргал бы состояние уничтоженного инстанса.
- Кастомный
requestне обязан зватьonProgress. Тогда итоговый объём берётся из размеров файлов, а не из нуля: «100%» приtotal: 0потребитель прочитал бы как «загружено ноль».
Доступность
Доступный контрол — сам нативный <input type="file">; зона роли-виджета не
получает, иначе input внутри неё теряется для скринридера (nested-interactive).
Фокус input’а зона показывает через focus-within, disabled гасится фоном, а не
opacity: прозрачность разбавляет выверенные на AA токены текста.
disabled и readonly приходят и от GrFormField, и от GrForm — не только
из собственных пропов. readonly при этом действительно запрещает ввод: набор
виден и уходит в форму, но диалог не откроется и drop не примут. Атрибута
readonly у <input type="file"> в HTML нет, поэтому системный диалог гасится
отменой действия по умолчанию — input остаётся в порядке Tab и объявляется
как «только чтение».
Фазы загрузки объявляет живой регион (role="status"), существующий с первого
рендера: регион, появляющийся сразу с текстом, часть AT не объявляет вовсе.
Прогресс в процентах туда не идёт — диктор захлебнётся.
Пропы этого примера