Обратная связь
Уведомление
Результат действия, которое запустил человек, — когда результату не место на экране, куда он смотрит. «Прогон поставлен в очередь», «настройки сохранены», «не удалось отправить».
Разметка
<div class="inst-toasts" style="position:static;inset:auto;pointer-events:auto">
<div class="inst-toast" data-tone="ok" role="status">
<div class="inst-toast-body">
<div class="inst-toast-title">Прогон поставлен в очередь</div>
<div class="inst-toast-text">worldbox-1 · седьмой в очереди</div>
</div>
<div class="inst-toast-actions">
<button class="inst-btn inst-btn--sm inst-btn--ghost" type="button">Показать</button>
</div>
</div>
<div class="inst-toast" data-tone="error" role="alert">
<div class="inst-toast-body">
<div class="inst-toast-title">Не удалось отправить</div>
<div class="inst-toast-text">Сеть недоступна. Попытка 3 из 5.</div>
</div>
</div>
<div class="inst-toast">
<div class="inst-toast-body">
<div class="inst-toast-title">Настройки сохранены</div>
</div>
</div>
</div>
Инлайновый стиль в примере — единственный на весь справочник, и он тут вынужденный: настоящая область живёт в верхнем слое и прижата к углу экрана, а показать её надо внутри кадра. В приложении так писать не нужно.
Контракт
Разметку строит instrument.js: тост заводится вызовом, а не написанным вручную узлом.
Если стандартной не хватает, соберите свой и положите в .inst-toasts сами.
| Что | Обязательно | Почему |
|---|---|---|
role="status", у ошибки role="alert" |
да | Ошибка перебивает, остальное сообщает вежливо |
duration: 0 у ошибки |
да | Сообщение о том, что действие не выполнено, не имеет права уйти незамеченным |
| Одно действие, не два | да | Тост уходит сам; выбор из двух вариантов требует времени, которого у него нет |
| Область одна на документ | да | popover="manual", открыта с инициализации |
Доступность
| Роль | status у обычного, alert у ошибки. Ошибка перебивает, остальное сообщает вежливо |
| Живой регион | Область существует и открыта до появления первого тоста. Регион, созданный одновременно с содержимым, не озвучивается |
| Пауза | Таймер замирает под курсором и на фокусе — WCAG 2.2.1 |
| Ошибка | duration: 0: сообщение о невыполненном действии не уходит само |
| Не только цвет | Тон несёт значок, а не одну заливку — тот же набор масок, что у сноски и баннера |
| Уменьшенное движение | Переход схлопывается; удаление подстраховано таймером, иначе тост остался бы навсегда |
| Клавиатура | Действие внутри — обычная кнопка и достижимо Tab. Область не перехватывает фокус: она сообщает, а не спрашивает |
| Печать | Уведомления на листе не печатаются: они по определению временные |
| Модалка | Тост под ней не виден. Сообщайте результат внутри диалога или после закрытия — см. выше |
Поведение
Почему popover
Область объявлена popover, потому что z-index не решает задачу целиком.
Тост обязан быть виден поверх всего, что нарисовало приложение: панели с
overflow: hidden, липкой шапки, открытого поповера. Верхний слой даёт это
по построению, а число в z-index — только до первого чужого числа побольше.
Отсюда же в библиотеке нет отдельного z-индекса для тостов: применить его было бы некуда.
Модалка выше тоста
Тост не перекрывает открытую модалку. Проверено замером: область,
переоткрытая после showModal() — и в той же задаче, и через задачу, —
остаётся под диалогом. Верхний слой упорядочен по времени входа, но
модальный диалог держится выше манипуляций с popover.
Что делать вместо:
| Ситуация | Как |
|---|---|
| Действие запущено из модалки и она закрывается | Закройте диалог, потом покажите тост |
| Модалка остаётся открытой | Результат — внутри неё: сноска или блок отказа в теле диалога |
| Долгая операция при открытой модалке | Мера внутри диалога, а тост — после закрытия |
Так даже правильнее: сообщение о результате действия, запущенного в диалоге, и должно приходить туда, куда смотрит человек.
Почему область открыта всегда
popover="manual" открывается один раз при инициализации и не закрывается.
Закрытый popover — это display: none, а живой регион в display: none
не озвучивается: скринридер не сообщил бы ни одного уведомления. Пустая
открытая область невидима и ничего не перехватывает — pointer-events
возвращаются только самим тостам.
Пауза и потолок очереди
Таймер замирает под курсором и на фокусе. Уведомление, исчезнувшее тогда, когда его начали читать, — это потерянное сообщение, и WCAG 2.2.1 требует этого: у времени должна быть пауза.
Больше четырёх одновременно не показывается — двадцать уведомлений подряд читаются как стена. Самые старые уходят, чтобы новое было видно.
JS
Тост не пишут разметкой — его зовут. Поэтому пример здесь один и он живой: нажмите, и уведомление придёт в правый нижний угол экрана.
Разметка
<button class="inst-btn inst-btn--primary" type="button"
data-demo-toast='{"tone":"ok","title":"Прогон поставлен в очередь","text":"worldbox-1 · седьмой в очереди"}'>Успех</button>
<button class="inst-btn" type="button"
data-demo-toast='{"tone":"error","title":"Не удалось отправить","duration":0}'>Ошибка, которая не уходит</button>
<button class="inst-btn" type="button"
data-demo-toast='{"tone":"running","title":"Идёт сборка","text":"Осталось два шага"}'>Идёт</button>
data-demo-toast — атрибут этого сайта, а не библиотеки: он нужен, чтобы
страница осталась разметкой. В приложении вы вызываете toast() из кода.
import { toast } from '@keshon/instrument/js';
toast({ tone: 'ok', title: 'Прогон поставлен в очередь',
text: 'worldbox-1 · седьмой в очереди' });
Без скрипта тостов нет вовсе: очередь, таймер и пауза под курсором — работа, которую CSS не выполняет. Вид, появление и уход при этом целиком на CSS.
Методы
toast(options) |
Показать уведомление. Возвращает узел — его можно убрать раньше срока |
Опции
| Поле | Что делает |
|---|---|
title |
Что произошло. Одна строка, без точки |
text |
Подробность. Необязательна |
tone |
ok · warn · error · running · neutral |
duration |
Миллисекунды. 0 — не уходит сам |
action |
{ label, onClick } — одно действие, не два |
toast({ tone: 'error', title: 'Не удалось отправить', duration: 0 });
toast({ title: 'Задача удалена',
action: { label: 'Вернуть', onClick: () => restore() } });
События
Своих нет. Действие приходит колбэком action.onClick.
API
| Имя | Что делает |
|---|---|
| класс | |
inst-toasts | Область. Одна на документ, объявлена popover="manual" |
inst-toast | Одно уведомление |
inst-toast-body | Текстовая часть |
inst-toast-title | Что произошло. Одна строка |
inst-toast-text | Подробность. Необязательна |
inst-toast-actions | Действие: «Отменить», «Показать» |
| модификатор | |
inst-toasts--start | Область у верхнего края. Умолчание — нижний |
| Имя | Значение | Что делает |
|---|---|---|
| атрибут | ||
data-tone | neutral · running · ok · warn · error | Значок и цвет. Без атрибута значка нет |
data-state | leaving | Ставит instrument.js перед удалением, чтобы переход доиграл |
role | status · alert | alert только у ошибки: она перебивает, остальное сообщает вежливо |
popover | manual | На области. Верхний слой, без лёгкого закрытия |
| Имя | Значение | Откуда |
|---|---|---|
| токен | ||
--surface-overlayПоверхность уведомления | light-dark(var(--n-0), var(--n-11)) | база +1: тема |
--shadow-popoverТретий и последний носитель тени в библиотеке | 0 4px 12px -2px var(--shadow-color-far), 0 2px 4px -2px var(--shadow-color-near) | одно на все ячейки |
Почему кит устроен именно так — конституция · Открытый код под MIT, github.com/keshon/instrument