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-019, ADR-021) окажется мало.
|
||||||
- Подписи сообщений вторым ключом — если потребуется защита от сговора участника комнаты с сервером (ADR-016).
|
- Подписи сообщений вторым ключом — если потребуется защита от сговора участника комнаты с сервером (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-чат.
|
- Выравнивание сообщений по стороне: свои — вправо, чужие — влево, как в обычных мессенджерах. Сейчас лента — общий список с колонкой автора (`docs/ui.md`, «Чат»; `docs/identity/brief.md`, «Компоновка»: сетка `132px 1fr` на десктопе, автор над группой на мобильном) — сторона у сообщения не задана нигде. Пользователям в тесте медленно читать, кто есть кто. Смена — это пересмотр принятого в ADR-024 макета ленты, нужен новый ADR, если решение — делать. Ограничение айдентики: без пузырей, скруглений и теней (`identity/brief.md`) — выравнивание, не bubble-чат.
|
||||||
|
|
||||||
Закрыто ADR-015…024: регистрация, контакты, лимиты, смена пароля, идентификация устройств, язык интерфейса, айдентика, доверие к ключам, протокол, схема базы, деплой, правила пушей.
|
Закрыто ADR-015…024: регистрация, контакты, лимиты, смена пароля, идентификация устройств, язык интерфейса, айдентика, доверие к ключам, протокол, схема базы, деплой, правила пушей.
|
||||||
|
|||||||
+12
-7
@@ -197,9 +197,16 @@ async function refreshRoom(view, known) {
|
|||||||
// composer — полоса состояния и строка ввода: рамка 1 px ink, слева «>»
|
// composer — полоса состояния и строка ввода: рамка 1 px ink, слева «>»
|
||||||
// цветом mark. Enter отправляет только на десктопе; на мобильном он делает
|
// цветом mark. Enter отправляет только на десктопе; на мобильном он делает
|
||||||
// перенос, а отправляет кнопка «>» справа (docs/ui.md, «Чат»).
|
// перенос, а отправляет кнопка «>» справа (docs/ui.md, «Чат»).
|
||||||
|
//
|
||||||
|
// div, не form: поле формы на iOS Safari/Chrome поднимает над клавиатурой
|
||||||
|
// системную панель навигации между полями («‹ ›» и «готово») — лишние
|
||||||
|
// ~50 px ради одного поля, которому переходить некуда. role="form" держит
|
||||||
|
// ту же семантику для скринридера (docs/ui.md, «Доступность»), без form-а;
|
||||||
|
// autocomplete="off" на поле — по той же причине, на случай если панель
|
||||||
|
// зависит ещё и от него.
|
||||||
function composer(view) {
|
function composer(view) {
|
||||||
const form = el("form", "compose");
|
const form = el("div", "compose");
|
||||||
form.noValidate = true;
|
form.setAttribute("role", "form");
|
||||||
|
|
||||||
view.bar = el("p", "bar");
|
view.bar = el("p", "bar");
|
||||||
view.bar.hidden = true;
|
view.bar.hidden = true;
|
||||||
@@ -213,12 +220,14 @@ function composer(view) {
|
|||||||
view.field.rows = 1;
|
view.field.rows = 1;
|
||||||
view.field.placeholder = "сообщение";
|
view.field.placeholder = "сообщение";
|
||||||
view.field.maxLength = view.limit;
|
view.field.maxLength = view.limit;
|
||||||
|
view.field.autocomplete = "off";
|
||||||
|
|
||||||
view.counter = el("span", "counter");
|
view.counter = el("span", "counter");
|
||||||
view.counter.hidden = true;
|
view.counter.hidden = true;
|
||||||
|
|
||||||
view.send = el("button", "input__send", ">");
|
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);
|
row.append(prompt, view.field, view.counter, el("span", "enter", "enter — отправить"), view.send);
|
||||||
form.append(view.bar, row);
|
form.append(view.bar, row);
|
||||||
@@ -234,10 +243,6 @@ function composer(view) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
submit(view);
|
submit(view);
|
||||||
});
|
});
|
||||||
form.addEventListener("submit", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
submit(view);
|
|
||||||
});
|
|
||||||
|
|
||||||
return form;
|
return form;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user