Именование
SheetContent не называет себя сам. Назвать шторку — ваша задача, и это обязательно: шторка без имени недоступна, потому что скринридеры объявляют её только как «dialog». Сошлитесь на видимый заголовок через aria-labelledby на SheetContent или задайте aria-label, если видимого заголовка нет. Незакрываемые шторки должны объяснять, зачем пользователь оказался в этом сценарии и как его завершить.
import { useId, useState, type CSSProperties } from 'react';
import {
Button,
Heading,
Icon,
IconButton,
Sheet,
SheetClose,
SheetContent,
SheetHandle,
SheetTrigger,
Text,Поведение клавиатуры
| Клавиша | Действие |
|---|---|
| EnterSpace | На триггере: открывает шторку. Фокус получает сама шторка или дочерний элемент с autoFocus. |
| TabShift + Tab | Переносит фокус на следующий или предыдущий элемент управления внутри шторки. Страница за ней инертна, и фокус на неё не попадает. SheetHandle без нескольких точек остановки отключён и пропускается. |
| EnterSpace | На SheetHandle с несколькими точками остановки: переводит шторку в следующую, более высокую точку, а из самой высокой — обратно в самую низкую. |
| Escape | Закрывает шторку и возвращает фокус на элемент, который её открыл. С closedBy='none' ничего не делает. |
SheetHandle — всегда <button> без собственного имени: дайте его через aria-label, например Resize sheet. Без нескольких точек привязки она disabled и aria-hidden, поэтому не объявляется, но шторку за неё всё ещё можно перетаскивать.
Поведение фокуса
- При открытии фокус должен переходить в шторку.
- При закрытии фокус должен разумно восстанавливаться.
Работа с фокусом обеспечивается нативным <dialog>: пока шторка открыта, остальная страница инертна, Tab остаётся внутри шторки (но может дойти до интерфейса браузера), а закрытие возвращает фокус на элемент, который был в фокусе до открытия.
При открытии фокус получает сама шторка, поэтому внутри ничего не подсвечено, а скринридеры объявляют её заголовок; Tab переходит к первому контролу. Поставьте autoFocus на дочерний элемент, чтобы фокусировать его, — например, на первое поле формы.
Семантика для скринридеров
- Браузер предоставляет шторку как
role=dialogсaria-modal, а имя ей дают вашиaria-labelledbyилиaria-label. - Триггер предоставляет
aria-haspopup='dialog'иaria-expanded. - У
SheetCloseнет метки по умолчанию, поэтому кнопке только с иконкой внутри неё нуженaria-label; встроенных строк библиотека не поставляет. - Шторка должна восприниматься как одна цельная поверхность оверлея.
- Незакрываемым шторкам нужна особенно понятная цель.
Семантика формы
- Если в шторке есть форма, обязательные поля и сообщения об ошибках должны оставаться видимыми на каждой точке привязки.
- Раскладка, удобная для жестов, не должна скрывать настоящую семантику отправки и закрытия.
- Кнопки в
<form method="dialog">закрывают шторку черезonChangeOpen(false), аvalueкнопки отправки становитсяreturnValueдиалога. Это работает и сclosedBy='none', так что у незакрываемой шторки всё равно есть путь завершения.
Риски доступности
- Шторка без
aria-labelledbyилиaria-label. - Опора только на понимание жестов.
- Точки привязки, при которых частично видимое содержимое становится непригодным.
- Незакрываемые сценарии без понятного пути завершения.