ADR-064: строка ввода чата — div вместо form
Убирает системную панель навигации по полям над клавиатурой на iOS (Safari и Chrome): она держится на элементе <form>, а не на textarea как таковом. Дешёвый вариант вместо contenteditable — сначала пробуем его, дороже (переписывать paste/placeholder/лимит руками) не потребовалось. Отправка теперь идёт по click на кнопке и по keydown Enter вместо события submit — форму убрали, role="form" держит ту же семантику для скринридера.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# ADR-064: строка ввода — div, а не form
|
||||
|
||||
Уточняет `docs/ui.md`, «Доступность»: `form` в списке семантики держит `role="form"`, а не элемент `<form>`, — для строки ввода чата.
|
||||
|
||||
## Контекст
|
||||
|
||||
Тест с реальными пользователями (iPhone, Safari и Chrome): над клавиатурой при фокусе в строке ввода всплывает системная панель навигации между полями — «‹ ›» и «готово», около 50 px. Полю переходить некуда — оно на экране одно, — а место съедено.
|
||||
|
||||
Панель — не наша вёрстка, это поведение WebKit для полей, лежащих внутри `<form>`. Строка ввода была оформлена именно так: `<form class="compose">` с `<textarea>` и кнопкой `type="submit"`.
|
||||
|
||||
Дорогой вариант — `contenteditable` вместо `textarea` — снял бы панель гарантированно, но потребовал бы вручную переписать то, что сейчас даёт браузер бесплатно: placeholder, обрезку на 4000 символов, автовысоту, и обязательно — очистку `paste` до текста, потому что `contenteditable` иначе вставляет форматированный HTML, а `innerHTML` в клиенте запрещён (CLAUDE.md).
|
||||
|
||||
Дешёвый вариант — вынести поле из `<form>` — не трогает ничего из этого: `textarea` остаётся `textarea`.
|
||||
|
||||
## Решение
|
||||
|
||||
- `<form class="compose">` → `<div class="compose" role="form">`. `role="form"` — тот же ориентир для скринридера, что и элемент `<form>`, без вызывающих панель heuristics WebKit.
|
||||
- Кнопка отправки — `type="button"` с явным `click`, вместо `type="submit"` и события `submit` на форме.
|
||||
- `autocomplete="off"` на самом поле — на случай, если панель зависит ещё и от него, а не только от `<form>`.
|
||||
- `textarea`, `maxLength`, `field-sizing: content`, placeholder — без изменений.
|
||||
|
||||
Остальные формы (вход, `#/new`, участники, настройки) не тронуты: там несколько полей в форме, панель навигации между ними — по крайней мере, не чистый минус, и незачем менять то, что не жаловались.
|
||||
|
||||
## Следствия
|
||||
|
||||
- Проверить можно только на реальном устройстве — эмуляции панели WebKit нет; headless-Chromium её и не показывал никогда, так что автотест здесь не поставить.
|
||||
- Если панель не уйдёт (вдруг триггер — не `<form>`, а что-то ещё в эвристике Safari), следующий шаг — `contenteditable`, со всей его ценой из «Контекста».
|
||||
- `Enter` на десктопе и кнопка «>» на мобильном по-прежнему отправляют — обработчики `keydown` и `click` делают то же, что раньше `submit`.
|
||||
@@ -5,7 +5,6 @@
|
||||
- Серверный «перец» для ключевого блоба: дополнительное шифрование блоба серверным ключом, хранящимся вне базы. Плюс: дамп базы сам по себе перестаёт быть материалом для оффлайн-перебора. Минус: не защищает от оператора; потеря серверного ключа — невозможность входа с новых устройств для всех. Решение отложено.
|
||||
- Блокировка собеседника и персональные инвайты — если общего инвайт-кода и лимитов (ADR-019, ADR-021) окажется мало.
|
||||
- Подписи сообщений вторым ключом — если потребуется защита от сговора участника комнаты с сервером (ADR-016).
|
||||
- Поле ввода — `textarea` в мобильном Safari поднимает системную панель навигации по полям («‹ ›» и «готово») над клавиатурой: живой экран съедает лишние ~50 px, обжито пользователями плохо. Направление — заменить на `contenteditable`-блок (`role="textbox"`, `aria-multiline="true"`): у него этой панели нет, а клавиатура остаётся текстовой. Ручная работа взамен браузерной: placeholder, счётчик и обрезка на 4000 символов, автовысота, `paste` только как текст — без этого блока `innerHTML` в него попадёт вставкой, а `innerHTML` в клиенте запрещён (CLAUDE.md). Enter/Shift+Enter и `isComposing` — переносятся как есть. Решение отложено: выбрать между этим и более дешёвым вариантом (поле вне `<form>`, `autocomplete="off"`) — если он снимает панель хотя бы в Safari, возни меньше.
|
||||
- Выравнивание сообщений по стороне: свои — вправо, чужие — влево, как в обычных мессенджерах. Сейчас лента — общий список с колонкой автора (`docs/ui.md`, «Чат»; `docs/identity/brief.md`, «Компоновка»: сетка `132px 1fr` на десктопе, автор над группой на мобильном) — сторона у сообщения не задана нигде. Пользователям в тесте медленно читать, кто есть кто. Смена — это пересмотр принятого в ADR-024 макета ленты, нужен новый ADR, если решение — делать. Ограничение айдентики: без пузырей, скруглений и теней (`identity/brief.md`) — выравнивание, не bubble-чат.
|
||||
|
||||
Закрыто ADR-015…024: регистрация, контакты, лимиты, смена пароля, идентификация устройств, язык интерфейса, айдентика, доверие к ключам, протокол, схема базы, деплой, правила пушей.
|
||||
|
||||
+12
-7
@@ -197,9 +197,16 @@ async function refreshRoom(view, known) {
|
||||
// composer — полоса состояния и строка ввода: рамка 1 px ink, слева «>»
|
||||
// цветом mark. Enter отправляет только на десктопе; на мобильном он делает
|
||||
// перенос, а отправляет кнопка «>» справа (docs/ui.md, «Чат»).
|
||||
//
|
||||
// div, не form: поле формы на iOS Safari/Chrome поднимает над клавиатурой
|
||||
// системную панель навигации между полями («‹ ›» и «готово») — лишние
|
||||
// ~50 px ради одного поля, которому переходить некуда. role="form" держит
|
||||
// ту же семантику для скринридера (docs/ui.md, «Доступность»), без form-а;
|
||||
// autocomplete="off" на поле — по той же причине, на случай если панель
|
||||
// зависит ещё и от него.
|
||||
function composer(view) {
|
||||
const form = el("form", "compose");
|
||||
form.noValidate = true;
|
||||
const form = el("div", "compose");
|
||||
form.setAttribute("role", "form");
|
||||
|
||||
view.bar = el("p", "bar");
|
||||
view.bar.hidden = true;
|
||||
@@ -213,12 +220,14 @@ function composer(view) {
|
||||
view.field.rows = 1;
|
||||
view.field.placeholder = "сообщение";
|
||||
view.field.maxLength = view.limit;
|
||||
view.field.autocomplete = "off";
|
||||
|
||||
view.counter = el("span", "counter");
|
||||
view.counter.hidden = true;
|
||||
|
||||
view.send = el("button", "input__send", ">");
|
||||
view.send.type = "submit";
|
||||
view.send.type = "button";
|
||||
view.send.addEventListener("click", () => submit(view));
|
||||
|
||||
row.append(prompt, view.field, view.counter, el("span", "enter", "enter — отправить"), view.send);
|
||||
form.append(view.bar, row);
|
||||
@@ -234,10 +243,6 @@ function composer(view) {
|
||||
event.preventDefault();
|
||||
submit(view);
|
||||
});
|
||||
form.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
submit(view);
|
||||
});
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user