Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(MaskedInput): (rev. 2) #3390

Merged
merged 86 commits into from
Sep 30, 2024
Merged

feat(MaskedInput): (rev. 2) #3390

merged 86 commits into from
Sep 30, 2024

Conversation

lossir
Copy link
Member

@lossir lossir commented Mar 26, 2024

❌ Проблемы

В первой редакции осталась старая логика отображения символов маски, когда эти символы рендерятся в спан-элементах поверх нативного инпута.

У такого способа есть неоспоримые плюсы:

  1. Не введённая часть маски не мешает пользователю работать с введённой частью.
  2. Символы нижнего подчёркивания реализованы стилями, что делает их независимыми от применяемого шрифта.

Но он также создаёт и проблемы.

  1. Мелкие баги. Сторонние пакета, которые мы выбирали для работы с маской, умеют рендерить символы маски только как часть value. Чтобы корректно вырезать не введённую часть маски приходилось лезть в их внутрянку и писать хитрую логику. Это создаёт баги и в Input mask и в первой MaskedInput.
  2. Ограничение возможностей пакета iMask. Хитрая логика из п1 переехала и в первую редакцию MaskedInput. Но удалось учесть только простые сценарии iMask, похожие на старые из Input mask.
  3. Усложнение тестирования. Визуально символы маски находятся внутри инпута, но фактически их там нет. Это приходится знать и учитывать при написании тестов.

✅ Новый концепт

MaskedInput должен вести себя как обычный Input

Такая идея лежит во второй ревизии компонента.

Теперь если символы маски видно, значит они физически присутствуют в инпуте. Если не видно, то их и нет. Как в обычном Input.

При этом введённое пользователем значение (темная часть) всё ещё доступно, но лежит в специальном атрибуте data-typed-value.

Кажется, что символы маски должны быть в value И они действительно все в value
image image

Чтобы символы подчёркивания не сливались в одну линию, мы вернулись к истокам - применили специальный шрифт на основе Lab Grotesque Regular.

Он доступен на статике под коротким название uihttps://s.kontur.ru/common-v2/fonts/ui/.

Мы же храним его в пакете в формате base64. Это избавит нас от лишней связности, и не потребует от пользователя каких-либо дополнительных настроек билдеров.

Потенциально такой способ тоже может работать с разными шрифтами и начертаниями.

Lab Grotesque Times New Roman
image image

Для покраски не введённых символов в серый используются CSS и немного shadow DOM.

Всю реализацию второй редакции можно представить в виде 3-х слоёв, где в основе всегда остаётся обычный инпут.

image CSS красит не введённую часть маски
image ui.woff стилизует символы подчёркивания
image Обычный input

🔍 Технические детали

В коде логика разделёна на 3 компонента:

  • MaskedInput — точка входа. отвечает за пропы и схожесть в поведении с обычным Input
  • FixedIMaskInput — исправляет некоторые проблемы iMask
  • ColorableInputElement — занимается покраской символов

ColorableInputElement

После отказа от поддержки IE11 удалось спрятать вёрстку символов маски.
Новый код минимально влияет на вёрстку, благодаря чему MaskedInput тестируется как обычный Input.

Работает всё с помощью градиента, стилей background-clip1 и -webkit-text-fill-color2, и shadow-dom.

В shadow-dom рендерится заполненная часть маски, что позволяет узнать её ширину в пикселях.
Затем с помощью linear-gradient введённая часть маски красится чёрным, и символы маски серым.
А стиль background-clip: text переносит цвет фона внутрь текста.

Вся реализация инкапсулирована в отдельный компонент <ColorableInputElement />.

Старый новый шрифт ui

Символ подчеркивания снова реализовал через специальный шрифт (ui), как это было в старых версиях пакета.
Но теперь шрифт лежит на сервере статики, что позволило избежать сложностей, которые он создавал раньше3.

У текущая реализации подключения шрифта есть ограничение, о котором написано ниже.

FixedIMaskInput

Используемый внутри пакет imask умеет предотвращать смешение символов, если использовать апостроф в маске.
Из-за этой фичи инпут всегда позволяет добавлять символы не только строго слева-направо, но и в середине маски.

Если эту фичу не использовать, то такое поведение больше похоже на баг, т.к. введённые в середине маски символы затем могут сместиться к ближайшему введённому символу слева.

Это всё противоречит текущему привычному поведение, когда с серыми символами маски невозможно взаимодействовать.

Всё исправлено в специальном компоненте <FixedIMaskInput />

📛 Оставшиеся ранее известные ограничения

Также есть некоторые проблемы/недоработки которые были и в предыдущей реализации <Input mask />, но на которые не жаловались.

Ломается маска при наличия скролла

Если value шире чем width компонента, то появится возможность скролить содержимое стрелками, мышкой, или скролиться будет само по мере ввода.

В предыдущей реализации ломалась вёрстка маски, и она могла пропадать частями.

В новой реализации css свойство background-clip: text всегда отображает текст будто у элемента scrollLeft = 0.
Настройки свойства background-position в этом случае тоже игнорируются.
Единственный способ исправить - отключать подсветку при наличие скролла.

Маску можно заполнять строго слева направо

В предыдущей реализации технически невозможно ввести символ, скажем, в середине маски.

В новой реализации принципиально такая возможность есть (см символ ` в imask), но сейчас подсветка работает по старой логике.
В принципе это можно реализовать, но надо ли.

🤕 Новые ограничения

eager=true

По Гайдам во время редактирования каретка должна автоматически перескакивать фиксированные символы (скобки, тире и тп).
Это достигается пропом eager=true пакета imask. Но в самом пакете imask возникает баг, при удалении по клавише delete — удаляются символы слева от каретки.
Это не удалось исправить внутри нашего компонента, поэтому пока используется проп eager=append.

Uncontrolled ввод и defaultValue

При появлении маски по фокусу ломается неконтролируемый ввод, если defaultValue содержит любую фиксированную часть маски.

<MaskedInput mask="+7 999 999-99-99" defaultValue="+7 12" />

Это происходит из-за динамической смены пропа lazy при фокусе.
Исправить это можно, есть использовать проп showOnFocus в компонент ColorableInputElement.
Тогда символы маски будут рендериться всегда, а их скрытие будет реализовано через CSS.

Но в таком случае MaskedInput будет отличаться от обычного Input, т.к. визуально отсутствующие символы маски будут находится в value.

Я посчитал что лучше пожертвовать довольно редким сценарием, чем лишаться удобства тестирования.

Шрифт ui подключается только при монтировании

Для красивого символа земли _ теперь используется специальный шрифт4.
Особенность его подключение в том, что основной шрифт должен быть явно указан в том же css правиле:

font-family: ui, Arial;

Для этого основной шрифт необходимо получить через getComputedStyle у родителя.

Проблема в том, что не существует общего/готового механизма, который бы оповещал о смене шрифта для конкретного элемента на странице.
Поэтому в текущей реализации основной шрифт берётся из body единожды, после чего шрифт ui подключается нужным элементам.

Ссылки

Демо работы покраски текста градиентом:
https://codepen.io/lossir/pen/ExrewOx

Чек-лист перед запросом ревью

  1. Добавлены тесты на все изменения
    ✅ unit-тесты для логики
    ✅ скриншоты для верстки и кросс-браузерности
    ⬜ нерелевантно

  2. Добавлена (обновлена) документация
    ✅ styleguidist для пропов и примеров использования компонентов
    ✅ jsdoc для утилит и хелперов
    ✅ комментарии для неочевидных мест в коде
    ✅ прочие инструкции (README.md, contributing.md и др.)
    ⬜ нерелевантно

  3. Изменения корректно типизированы
    ✅ без использования any (см. PR 2856)
    ⬜ нерелевантно

  4. Прочее
    ✅ все тесты и линтеры на CI проходят
    ✅ в коде нет лишних изменений
    ✅ заголовок PR кратко и доступно отражает суть изменений (он попадет в changelog)

Footnotes

  1. https://caniuse.com/background-clip-text

  2. https://caniuse.com/mdn-css_properties_-webkit-text-fill-color

  3. https://github.com/skbkontur/retail-ui/issues/2181

  4. https://s.kontur.ru/common-v2/fonts/ui/

lossir added 29 commits March 20, 2024 14:52
@lossir lossir changed the title feat(MaskedInput): 2nd edition feat(MaskedInput): (rev. 2) Sep 23, 2024
@lossir lossir changed the base branch from master to next September 23, 2024 13:56
@zhzz
Copy link
Member

zhzz commented Sep 27, 2024

Нужно продублировать этот ПР в 5.x. Выкатывать будем в 5.0, т.к. изменения ломающие. Этот предлагаю потом просто закрыть.

@lossir lossir changed the base branch from next to 5.x September 27, 2024 16:55
@lossir
Copy link
Member Author

lossir commented Sep 27, 2024

Этот предлагаю потом просто закрыть.

Попробую просто сменить base ветку. 5.x же отпачкована от next, и в 5.x никто не трогал MaskedInput.
Несовместимых расхождений быть не должно.

lossir and others added 10 commits September 28, 2024 00:21
…kedInput-fixes

# Conflicts:
#	packages/react-ui/.creevey/images/MaskedInput/Mask/firefox2022.png
#	packages/react-ui/.creevey/images/MaskedInput/Mask/firefox2022Dark.png
#	packages/react-ui/.creevey/images/MaskedInput/Validations/chrome2022.png
#	packages/react-ui/.creevey/images/MaskedInput/Validations/chrome2022Dark.png
#	packages/react-ui/.creevey/images/MaskedInput/Validations/firefox2022.png
#	packages/react-ui/.creevey/images/MaskedInput/Validations/firefox2022Dark.png
#	packages/react-ui/components/Input/__stories__/Input.creevey.stories.tsx
#	packages/react-ui/components/MaskedInput/__stories__/MaskedInput.creevey.stories.tsx
#	packages/react-ui/components/MaskedInput/__stories__/MaskedInput.stories.tsx
#	packages/react-ui/components/MaskedInput/__tests__/MaskedInput-test.tsx
#	packages/react-ui/package.json
#	packages/react-ui/test-setup.js
#	yarn.lock
@zhzz zhzz merged commit b2fb84f into 5.x Sep 30, 2024
7 checks passed
@zhzz zhzz deleted the IF-1701-MaskedInput-fixes branch September 30, 2024 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants