Files
bare/docs/decisions/069-message-input-is-editable-block.md
T
mayatnikovandClaude Opus 5 383e374f0f iOS: запрет масштабирования, версия в подвале, самообновление, строка ввода
Отзыв с iPhone после первого живого прогона.

Масштабирование запрещено (ADR-066): viewport как в соседнем sixlines плюс
viewport-fit=cover, мета-теги установленного приложения, touch-action против
двойного тапа и страховка на JS — гашение жестов iOS, потому что мета-строку
Safari вправе проигнорировать. Размеры шрифтов не тронуты: 14 px из брифа
остаются, поднимать поля до 16 px владелец отклонил. Цена — доступность
для тех, кому зум нужен — записана в ADR и в ui.md честно.

Версия в подвале (ADR-065, 067): GET /api/config отдаёт короткую ревизию
и время коммита. Именно коммита, а не сборки: штамп компиляции в бинаре
means каждая пересборка даёт новый хеш, и обещание ADR-022 «бинарь собирается
из тега байт в байт» перестаёт проверяться.

Самообновление PWA (ADR-068, 070): в режиме приложения на iPhone перезагрузки
нет, и старая версия жила бы вечно. Проверка при возврате в приложение,
включение ждущей оболочки, перезагрузка — но не тогда, когда в строке набран
текст. Замок от петли сравнивает версию оболочки, а не считает перезагрузки:
счётчик отменял саму фичу, долгоживущая вкладка переставала обновляться.
Замерено: было 30, 60, 120, 240 секунд между релизами, стало ровно 30.

Строка ввода — редактируемый блок (ADR-069): iOS рисует над клавиатурой
полосу со стрелками и «готово» для полей формы, и убрать её со страницы
нельзя. Для contenteditable не рисует. Режим plaintext-only с проверкой,
что он применился, и запасным путём с разбором вставки: невалидное значение
атрибута в части браузеров делает блок нередактируемым вовсе. Чтение
и запись только через textContent, innerHTML в клиенте по-прежнему нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-23 15:11:13 +03:00

37 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-069: Строка сообщения — редактируемый блок, а не поле формы
Уточняет [ADR-024](024-identity-and-ui.md) («Чат» и «Доступность» в `docs/ui.md`) и [ADR-066](066-no-zoom-app-feel.md): в перечне элементов управления, по которым второй тап не гасится, прибавился редактируемый блок.
Уточнён [ADR-071](071-input-limit-cuts-what-arrives.md): предел держится до вставки и режет приходящее, а не хвост блока.
## Контекст
На iPhone при фокусе в строке сообщения над клавиатурой висит системная полоса помощника форм: две стрелки перехода между полями и «готово». Стрелки неактивны — поле на экране одно, переходить некуда, — а полоса занимает место и ломает ощущение приложения, ради которого принят ADR-066.
Убрать её со страницы нечем: полосу рисует система, и для `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`, «Чат» и «Доступность».