Files
bare/docs/decisions/069-message-input-is-editable-block.md
T
mayatnikovandClaude Opus 5 f9ac83bef8 Развести номера ADR: 064/065/066 этой ветки → 073/074/075
Ветка отведена от 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
2026-08-24 11:23:12 +03:00

7.3 KiB
Raw Blame History

ADR-069: Строка сообщения — редактируемый блок, а не поле формы

Уточняет ADR-024 («Чат» и «Доступность» в docs/ui.md) и ADR-075: в перечне элементов управления, по которым второй тап не гасится, прибавился редактируемый блок.

Уточнён ADR-071: предел держится до вставки и режет приходящее, а не хвост блока.

Контекст

На 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 переносит, отправляет кнопка «>». enterkeyhintenter: подпись клавиши обещает то, что клавиша делает, а 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, «Чат» и «Доступность».