8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 203,
|
||
"slug": "editorial-2022-05-mechanism-design-system",
|
||
"title": "Контракт кнопки: как не потерять поведение между токеном и интерфейсом",
|
||
"excerpt": "Кнопка расходится по цвету, состояниям и доступности, когда разные слои владеют одним действием. Разбираем малый контракт, отрицательный путь и критерий готовности изменения.",
|
||
"contentHtml": "<p>На одном экране кнопка «Сохранить» показывает loading, на другом принимает второй клик. В третьем варианте клавиатурный фокус почти не виден. Иконка выглядит одинаково, но её смысл зависит от скрытого текста. Такие сбои часто появляются после небольшой правки: кто-то поменял цвет, другой слой добавил <code>disabled</code>, а локальный компонент оставил старый обработчик.</p>\n<p>Цена ошибки выше, чем разница в CSS. Повторный клик может отправить действие дважды. Потерянный focus мешает пройти форму без мыши. Неясное имя кнопки ломает сценарий для screen reader. Команда при этом видит несколько похожих компонентов и не знает, какой из них задаёт правило.</p>\n<p><strong>Тезис.</strong> Малой дизайн-системе нужен не большой каталог компонентов, а явный контракт одного control. Контракт разделяет четыре вида данных: значения токенов, допустимые состояния, семантику и известные места применения. Каждый слой имеет владельца. Ни один слой не притворяется проверкой браузера.</p>\n<h2>Механизм: четыре слоя одного действия</h2>\n<p>Токены хранят именованные значения: фон, цвет текста, контур фокуса, радиус и отступ. Токен не знает, завершился ли запрос. Если в нём появляется флаг loading, он начинает управлять чужим поведением.</p>\n<p>Слой состояний перечисляет допустимые ветки: <code>default</code>, <code>hover</code>, <code>focus-visible</code>, <code>disabled</code> и <code>loading</code>. Список не доказывает, что браузер реально показал каждую ветку. Он только не даёт молча забыть обязательный сценарий.</p>\n<p>Семантический слой описывает <code>name</code>, <code>role</code>, текущее состояние и признак недоступности. Цвет не заменяет эти поля. Серый фон может означать disabled, loading или ошибочный стиль. Для простой кнопки стоит начать с native <code><button></code>: он уже имеет базовую клавиатурную модель. <code>role="button"</code> на другом элементе не воспроизводит её автоматически.</p>\n<p>Инвентарь хранит известные usage. Например, <code>profile-save</code>, <code>billing-pay</code> и <code>dialog-cancel</code> могут выглядеть похоже, но иметь разный риск. Инвентарь показывает предполагаемый радиус правки. Он не доказывает, что в репозитории нет четвёртого usage.</p>\n<div class='table-scroll'><table><caption>Границы малого контракта кнопки</caption><thead><tr><th scope='col'>Слой</th><th scope='col'>Владеет</th><th scope='col'>Не доказывает</th><th scope='col'>Проверка за границей</th></tr></thead><tbody><tr><td>Токены</td><td>Имена и значения background, foreground, focus ring, radius, gap</td><td>Что собранный CSS применился во всех вариантах</td><td>Собранный CSS и visual diff в выбранной среде</td></tr><tr><td>Состояния</td><td>Набор default, hover, focus-visible, disabled, loading</td><td>Что пользователь увидел каждую ветку</td><td>Сценарий мышью и клавиатурой или автоматизация</td></tr><tr><td>Семантика</td><td>Имя, роль, state и disabled</td><td>Что screen reader произнёс ожидаемую фразу</td><td>DOM, accessibility tree и выбранная технология</td></tr><tr><td>Инвентарь</td><td>Явно известные места применения</td><td>Полноту поиска по кодовой базе</td><td>Поиск, классификация и review миграции</td></tr><tr><td>Visual payload</td><td>Viewports, states и имена токенов для будущего запуска</td><td>Screenshot, diff, score или regression</td><td>Реальный runner с сохранённым артефактом</td></tr></tbody></table></div>\n<h2>Почему имя CSS-переменной не создаёт семантику</h2>\n<p>CSS Custom Properties задают пользовательские свойства и позволяют подставлять их через <code>var()</code>. Спецификация не решает, что означает имя. <code>--button-primary-background</code> технически допустимо, но этого мало. Команда должна отдельно договориться, для какой роли живёт значение, кто его меняет и какие usage зависят от него.</p>\n<p>Полезная цепочка выглядит так: <code>button.primary.background</code> → primary button → конкретные usage. Она не требует одного способа реализации. Значение может попасть в CSS custom property, объект темы или stylesheet. Важно сохранить связь между ролью и радиусом изменения.</p>\n<p>Это также объясняет отрицательный путь. Если usage просит <code>button.primary.shadow</code>, а такого токена нет, валидатор должен вернуть ошибку. Быстрое добавление нового ключа скрывает решение о дизайне и расширяет общий API. Если значение имеет неверный формат, например <code>brand-blue</code> вместо ожидаемого учебного <code>#RRGGBB</code>, изменение тоже нужно остановить.</p>\n<h2>Семантика и состояние не выводятся из цвета</h2>\n<p>Поле <code>state</code> в контракте — объявленное намерение. Оно не равно состоянию DOM. Запись <code>loading</code> ещё не блокирует клик, не меняет доступность и не объявляет прогресс. Эти эффекты должен реализовать компонент, а затем пройти отдельную проверку.</p>\n<p>Для иконки без видимого текста нужно задать доступное имя. Для кнопки с текстом «Сохранить» имя обычно берётся из текста. Для icon-only control потребуется label или другая согласованная семантика. Не стоит рассчитывать на имя файла, title в CSS или цвет рядом с иконкой.</p>\n<p>Если команда выбирает custom host вместо native button, она принимает дополнительный долг: нужно проверить фокус, активацию клавишей Enter или Space, disabled-поведение и передачу имени. В таких случаях запись <code>role="button"</code> — только часть условия.</p>\n<h2>Исполнимый пример: проверка данных до UI</h2>\n<p>Ниже демонстрационный JavaScript-фрагмент. Он проверяет только согласованность описания. В нём нет DOM, CSS cascade, браузера и assistive technology. Успешный результат не означает, что кнопка доступна или визуально одинакова.</p>\n<pre><code>const system = {\n tokens: {\n 'button.primary.background': '#2457D6',\n 'button.primary.foreground': '#FFFFFF',\n 'button.focus.ring': '#111827'\n },\n requiredStates: ['default', 'hover', 'focus-visible', 'disabled', 'loading'],\n button: { name: 'Сохранить', role: 'button', state: 'default', disabled: false },\n usage: ['profile-save', 'billing-pay', 'dialog-cancel'],\n visualPayload: { viewports: [375, 1280], states: ['default', 'focus-visible'] }\n};\n\nfunction checkContract(value) {\n const statesOk = value.requiredStates.includes('focus-visible');\n const semanticsOk = value.button.role === 'button' && value.button.name.length > 0;\n const tokensOk = Object.keys(value.tokens).every((name) => name.startsWith('button.'));\n const usageOk = value.usage.length > 0;\n return { ok: statesOk && semanticsOk && tokensOk && usageOk };\n}\n\nconsole.log(checkContract(system)); // { ok: true }</code></pre>\n<p>Этот код полезен как ранняя защита границы. Если удалить <code>focus-visible</code>, имя кнопки или добавить токен с неизвестным namespace, проверка должна стать красной. Но она не проверяет повторный submit, computed style, contrast, реальное имя в accessibility tree или поведение на телефоне.</p>\n<figure><img src='/assets/editorial/2022/design-system-component-contract-2022.svg' alt='Схема контракта primary button: токены, обязательные состояния, семантические поля и инвентарь usage соединены до внешних проверок браузера и доступности.' loading='lazy' /><figcaption>Контракт удерживает четыре слоя отдельно. Внешние проверки подтверждают то, чего модель данных сама увидеть не может.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><table><caption>Диагностика расхождения кнопки</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>Loading показывает правильный цвет, но второй клик проходит</td><td>Состояние описали в style layer, а обработчик не использует его</td><td>Два последовательных события и проверка network calls</td><td>Связать loading с блокировкой действия и проверить повторный submit</td></tr><tr><td>В одном usage пропал focus ring</td><td>В state matrix нет focus-visible или его токен заменил literal</td><td>Пройти сценарий только клавиатурой и посмотреть computed style</td><td>Вернуть обязательное состояние и named token, затем повторить сценарий</td></tr><tr><td>Две кнопки с одинаковым цветом имеют разный смысл</td><td>Роль вывели из visual variant</td><td>Сравнить name, role, state и действие в DOM</td><td>Разделить contract или исправить семантические поля</td></tr><tr><td>Новый token проходит локальный review, но ломает сборку</td><td>Usage ссылается на undeclared key</td><td>Сверить ссылку с реестром имён и запустить валидатор</td><td>Остановить change; сначала объявить роль и владельца токена</td></tr><tr><td>В задаче есть «visual passed», но нет снимка</td><td>Payload перепутали с результатом runner</td><td>Найти screenshot, diff, browser и commit в артефакте</td><td>Пометить проверку как не выполненную и запустить её отдельно</td></tr></tbody></table></div>\n<h2>Порядок действий</h2>\n<ol><li><strong>Зафиксируйте симптом.</strong> Назовите один control, одно состояние, один usage и наблюдаемое действие. Не начинайте с общего «кнопки выглядят по-разному».</li><li><strong>Определите цену.</strong> Запишите, что может произойти: повторная операция, недоступная клавиатура, неверное имя или незаметный общий blast radius.</li><li><strong>Разложите владельцев.</strong> Отдельно выпишите токен, state matrix, semantic fields и inventory. Если один файл владеет всем сразу, отметьте это как риск.</li><li><strong>Проверьте отрицательный путь.</strong> Перед допустимой правкой отправьте неизвестный token и неверное значение. Убедитесь, что валидатор отказал и baseline не изменился.</li><li><strong>Сделайте одну правку.</strong> Вынесите literal в существующий токен, добавьте пропущенное state или исправьте native host. Не совмещайте это с полной миграцией библиотеки.</li><li><strong>Проверьте реальный control.</strong> Откройте страницу в согласованных browser и viewport, пройдите mouse и keyboard сценарии, проверьте DOM и accessibility tree.</li><li><strong>Сохраните артефакты.</strong> Для visual проверки нужны screenshot и diff; для доступности — зафиксированный сценарий и инструмент. Payload оставьте списком входов.</li><li><strong>Подготовьте откат.</strong> Запишите прежнее значение токена и ограничьте изменение известным inventory. Если появился неожиданный effect, верните только эту правку.</li></ol>\n<h2>Ограничения</h2>\n<p>Малый контракт не заменяет дизайн-систему целиком. Он не решает темизацию, dark mode, локализацию, responsive layout, сложную анимацию, права пользователя и сетевой submit. Он не определяет, какие действия должны быть destructive или toggle. Сходство радиуса и цвета не делает controls одним компонентом.</p>\n<p>Инвентарь остаётся неполным, если команда не проверила кодовую базу. Результат поиска тоже не идеален: стилизованная ссылка может выглядеть как button, а роль может появиться через обёртку. Поэтому inventory нужно помечать как подтверждённый или предполагаемый.</p>\n<p>Демонстрационные значения в коде выбраны для объяснения механизма. Они не являются production-рекомендацией и не показывают реальный результат visual, accessibility или пользовательского теста. Такой предел делает вывод проверяемым: мы утверждаем согласованность модели, а не качество конкретного интерфейса.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово, когда одновременно выполнены четыре условия: валидатор принимает contract и отклоняет отрицательный пример без изменения baseline; нужный usage внесён в инвентарь; реальный control проходит keyboard, pointer и accessibility-проверку в согласованной среде; visual-проверка имеет сохранённые screenshot и diff либо явно отмечена как ещё не выполненная. Дополнительно известны прежнее значение токена и точка отката.</p>\n<p>Если отсутствует хотя бы один внешний артефакт, готовность ограничивается моделью данных. Нельзя называть её visual или accessibility regression result. Этот язык сохраняет связь между тем, что команда описала, и тем, что она действительно наблюдала.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://www.w3.org/TR/css-variables-1/' target='_blank' rel='noopener noreferrer'>W3C: CSS Custom Properties for Cascading Variables Module Level 1</a> — описание custom properties и функции <code>var()</code>. Спецификация не назначает им дизайн-семантику.</li><li><a href='https://www.w3.org/TR/wai-aria/' target='_blank' rel='noopener noreferrer'>W3C: Accessible Rich Internet Applications (WAI-ARIA) 1.2</a> — нормативное описание ролей, состояний и свойств. Оно не заменяет проверку native HTML и assistive technology.</li><li><a href='https://www.w3.org/TR/WCAG22/' target='_blank' rel='noopener noreferrer'>W3C: Web Content Accessibility Guidelines (WCAG) 2.2</a> — критерии доступности, включая клавиатурный доступ, видимый фокус и имя/роль/значение. Учебный фрагмент выше их не измеряет.</li></ul>"
|
||
}
|