Ветка отведена от 522ba89, где последним был ADR-063, и заняла
064–072. Тем временем в main через #3 и #4 пришли свои 064
(строка ввода — div вместо form), 065 (стороны сообщений)
и 066 (серверного перца нет).
Переезжают три ADR этой ветки, 067–072 остаются на месте:
их номера свободны, а внутренние ссылки менять незачем.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR8uS3zGkybzWRW4GEY1oz
37 lines
7.3 KiB
Markdown
37 lines
7.3 KiB
Markdown
# ADR-069: Строка сообщения — редактируемый блок, а не поле формы
|
||
|
||
Уточняет [ADR-024](024-identity-and-ui.md) («Чат» и «Доступность» в `docs/ui.md`) и [ADR-075](075-no-zoom-app-feel.md): в перечне элементов управления, по которым второй тап не гасится, прибавился редактируемый блок.
|
||
|
||
Уточнён [ADR-071](071-input-limit-cuts-what-arrives.md): предел держится до вставки и режет приходящее, а не хвост блока.
|
||
|
||
## Контекст
|
||
|
||
На iPhone при фокусе в строке сообщения над клавиатурой висит системная полоса помощника форм: две стрелки перехода между полями и «готово». Стрелки неактивны — поле на экране одно, переходить некуда, — а полоса занимает место и ломает ощущение приложения, ради которого принят ADR-075.
|
||
|
||
Убрать её со страницы нечем: полосу рисует система, и для `input` и `textarea` она бывает всегда. Единственный работающий приём — перестать быть полем формы: у редактируемого блока (`contenteditable`) iOS полосы не показывает.
|
||
|
||
Цена приёма известна заранее. Блок принимает вставку с разметкой, ставит на Enter `<div>` и `<br>`, не знает ни `placeholder`, ни `maxLength`, ни `disabled`, а `innerHTML` в этом клиенте запрещён (ADR-001, CSP): сообщения — пользовательские данные.
|
||
|
||
## Решение
|
||
|
||
- Блоком становится только строка сообщения в чате. Вход, регистрация, смена пароля и «новый чат» остаются на настоящих `input`: там нужен `type="password"` для менеджеров паролей, а полоса на разовом экране не мешает.
|
||
- Блок открывается значением `plaintext-only`: в нём браузер кладёт внутрь только текст, чем бы ни был буфер обмена. Значение не назначается, а проверяется чтением — незнакомое значение атрибута делает блок нередактируемым вовсе, то есть строка ввода перестала бы работать молча. Не применилось — остаётся `true`, и тогда вставку, перетаскивание и перенос строки разбирает сам клиент: текст берётся из `text/plain` и вписывается узлом текста.
|
||
- Значение читается и пишется только `textContent`. `innerHTML` не появляется ни здесь, ни где-либо ещё.
|
||
- Подсказка — правило CSS `:empty::before` с `content: attr(...)`; текст кладётся атрибутом `data-*`. Опустевший блок очищается от `<br>`, который оставляет в нём браузер: с лишним узлом `:empty` не срабатывает и подсказки не видно.
|
||
- Клавиши прежние: на десктопе Enter отправляет, Shift+Enter переносит; на мобильном Enter переносит, отправляет кнопка «>». `enterkeyhint` — `enter`: подпись клавиши обещает то, что клавиша делает, а `send` обещал бы отправку, которой по нажатию не будет.
|
||
- `autocapitalize="sentences"` и `autocorrect="on"`: сообщение — обычная речь, и заглавная в начале предложения с исправлением опечаток тут к месту. В форме входа и в «новом чате» они, наоборот, выключены — там ник и пароль.
|
||
- Предел 4000 символов и счётчик после 3500 держит сам блок: длина считается по `textContent`, лишнее обрезается у того, что приходит в блок, — так же, как считал `maxLength` у `textarea` (ADR-071).
|
||
- Заблокированный ввод (предупреждение о ключе, уход из комнаты) перестаёт быть редактируемым и говорит об этом `aria-disabled`: `disabled` у блока нет.
|
||
- Доступность: `role="textbox"`, `aria-multiline="true"`, подпись `aria-label` тем же текстом, что подсказка; цель нажатия на мобильном не меньше 44 px; фокус при открытии чата на десктопе — как был.
|
||
- `web/js/zoom.js`: блок добавлен в перечень элементов управления. Гашение второго тапа уносит с собой `click`, а с ним и курсор — второй тап подряд не ставил бы курсор в строку ввода.
|
||
- `web/js/pwa.js`: проверка «есть ли черновик» (ADR-068) смотрит и в редактируемый блок — `value` у него нет, набранное лежит в `textContent`. Считается и заблокированный блок: недописанное в нём остаётся.
|
||
|
||
## Следствия
|
||
|
||
- Полоса помощника форм в чате исчезает. На экранах входа и «нового чата» она остаётся — там она и не мешает.
|
||
- Проверить это можно только на iPhone: Chrome такой полосы не рисует вовсе, и никакой прогон её не увидит.
|
||
- Строка ввода перестала быть элементом формы. Отправку это не меняет: Enter и раньше обрабатывал клиент, а кнопка «>» остаётся кнопкой отправки формы.
|
||
- В запасном пути (браузер без `plaintext-only`) отмена набранного (cmd+z) не помнит нашей вставки: она делается руками, а не `execCommand` — тот в Chrome разбирает перенос строки в `<div>`, чего в блоке быть не должно. Там же держится `<br>`-заполнитель последней строки: перенос в самом конце браузер не рисует, и без заполнителя курсор оставался бы на прежней строке.
|
||
- Автозаполнение и менеджеры паролей блока не касаются — в чате им нечего заполнять.
|
||
- Записано в `docs/ui.md`, «Чат» и «Доступность».
|