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
fix(Popup): displaying SVG icons #2946
Conversation
packages/react-ui/lib/SSRSafe.ts
Outdated
export function isHTMLElement(el: unknown): el is HTMLElement { | ||
if (isBrowser) { | ||
return el instanceof HTMLElement; | ||
return el instanceof HTMLElement || el instanceof SVGElement; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Так делать не стоит. Название функции и возвращаемый тип не отражает действительность. Это тоже нам может аукнуться.
Для этих узлов есть общая спецификация - Element
(HTMLElement
, SVGElement
)
Надо добавить новую функцию isElement()
и явно использовать вместо isHTMLElement
, где это уместно.
Пока писал тесты для getRootNode
тоже наткнулся на проблему с svg
. Я тоже добавлю isElement()
=)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Так делать не стоит. Название функции и возвращаемый тип не отражает действительность. Это тоже нам может аукнуться.
Почему название оставил прежним, я описал в секции "решение", но думаю, чтобы полностью исключить путаницу - можно назвать функцию isHTMLOrSVGElement
А вот про возвращаемый тип я как-то я упустил объяснение.. В общем, суть такая, что сейчас мы не можем заменить возвращаемый тип, так как это уже "аукнулось" нам. Сейчас для изменения возвращаемого типа нужно вносить в код ломающие изменения. Плюс, с типом Element
это будет сделать гораздо сложнее, чем с типом HTMLElement | SVGElement
, так как Element
это общий тип и в нём отсутствуют многие специфичные для HTMLElement
и SVGElement
сущности
Для этих узлов есть общая спецификация - Element (HTMLElement, SVGElement)
И так как в идеальном мире возвращаемый тип у нас HTMLElement | SVGElement
, то и проверка на instanceof Element
нам не совсем подходит из-за того, что мы вроде как проверяем является ли переданный элемент SVG
или HTML
элементом, а по факту смотрим на того является ли элемент инстансом Element
. То есть с точки зрения работы обе версии выдадут одинаковый результат, но версия с проверкой на HTMLElement
+ SVGElement
логичнее выглядит и проще для понимания
Изменил название функции и добавил пояснение про возвращаемый тип
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Если в возвращаемый тип не добавить SVGElement
, то это потенциально ломающая штука.
Например, в компоненте Menu
есть такой код:
this.scrollContainer.scrollTo(getRootNode(this.highlighted)); |
Который дёргает свойство offsetLeft
, и ниже ещё и offsetTop
retail-ui/packages/react-ui/components/ScrollContainer/ScrollContainer.tsx
Lines 142 to 149 in 772e647
public scrollTo(element: Nullable<HTMLElement>) { | |
if (!element || !this.inner) { | |
return; | |
} | |
this.inner.scrollLeft = element.offsetLeft; | |
this.inner.scrollTop = getScrollYOffset(element, this.inner); | |
} |
Но для SVGElement
эти свойства задикрикечены: offsetLeft, offsetTop
Интерфейс Element
наследуют только HTMLElement
, SVGElement
и MathMLElement
(по крайней мере других я не нашёл).
Все 3 типа элементов можно получить через законные рефки Реакта. Хоть у MathML
сейчас ограниченная поддержка, но она только увеличивается.
Поэтому, добавить отдельную функцию isElement
мне кажется уместным решением. В описании можно указать про HTMLElement
, SVGElement
и MathMLElement
. Надо будет везде явно указать типы и разрулить проблемы.
Я бы занялся этим в своем ПРе #2939, т.к. уже напоролся на все эти проблемы с типами.
Можем созвониться и обсудить.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Что по итогу было сделано:
-
Заменил функцию
isHTMLOrSVGElement
наisElement
-
Везде, где надо было, заменил тип
HTMLElement
наElement
-
Чтобы задача не разрасталась и не задерживалась, пофиксил тип только в тех местах, которые мешали работе функции
isElement
. Для исправления остальных случаев завёл задачу IF-643 -
Привёл тип к
HTMLElement || SVGElement
там, где нужен был вызов нативных методов (таких какfocus
) изgetRootNode
-
Поправил тип в
FocusEventType
, поправил и расширил тип вMouseEventType
-
Заменил обращение к
offsetLeft
,offsetTop
,offsetWidth
и остальныхoffset*
на получение значения изgetBoundingClientRect
. Эти сущности в большинстве случаев возвращают одинаковые значения, за исключением случая, когда к элементу применяется стильtransform
. Например, если к элементу сwidth: 100px
будет применён стильtransform: scale(0.5);
, тоoffsetWidth
вернёт100
, аgetBoundingClientRect
вернёт50
. Это может поломать интерфейсы, которые уже завязались на этом баге, но кажется, что это крайне маловероятно + лучшая альтернативаgetBoudingClientRect
которую я нашёл это вот этот сниппет, но при его тестировании оказалось, что работает он крайне так себе и часто возвращает неправильные значенияТакже,
getBoundingClientRect
возвращает дробное значение, аoffset*
- целое, поэтому, чтобы избежать неожиданных багов, округлил к нижней границе значения получаемые изgetBoundingClientRect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Дополнение к пункту 6:
После написания прошлого комментария обнаружилась ещё одна проблема с getBoundingClientRect
vs offset*
: если использовать getBoundingClientRect
в контейнерах со скроллом, то чтобы получить значение равное offsetLeft
нужно произвести вычисление getDOMRect(element).right - getDOMRect(element).left
Ещё не углублялся в причины этого поведения, но думаю, что это лучше решать в рамках отдельной задачи, так как проблема может оказаться гораздо глубже и замедлить выкатку этого пулл-реквеста. Пока оставил в местах, где обнаружился баг - offset*
, и завёл задачу IF-647, в рамках которой буду разбираться с этой проблемой
|
||
return ( | ||
<> | ||
{/* Manual opening used here as a safer way of performing logic. Opening hint manually does not alter the results of behavior that being tested */} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ручное управление уже тестируется в юнит тесте. Ещё есть такая стори SetManualAndOpenedPropOnClick
со скриншотами.
Думаю, здесь достаточно сразу рендерить открытый Хинт, для проверки работы с свг-шкой.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тогда мы не будем проверять пользовательский сценарий, а также не покроем все кейсы, из-за которых хинт может не отрендериться на свг-шке. В задаче хинт рендерился не только из-за того, что мы не могли найти его ноду, а также из-за того, что на него не передавались слушатели событий
Хотя, исходя из этой же логики текущая реализация не является корректной. Следовательно, единственный правильный вариант реализации этого теста - проверять отображается ли хинт при ховере
Обновил тест, а также заскипал скриншоты везде кроме хрома, чтобы сократить нагрузку. Думаю, что одного хрома хватит для тестирования логики
…fix/hint-with-svg-icon
…fix/hint-with-svg-icon
Проблема
Если передать
SVG
иконку напрямую вHint
- подсказка на иконке отображаться не будетКод для воспроизведения:
При этом если обернуть тег
<svg />
в любой другой тег (кроме<svg />
) - проблема уйдётРешение
Решение оказалось достаточно тривиальным (оно занимает всего одну строчку), а вот его поиск был не так тривиален
Окончательное решение:
Проблема была в том, что утилита
isHTMLElement
, которая у нас используется вPopup
иgetRootNode
(на парковке она не используется) проверяла только инстансы отHTMLElement
. НоSVG
иконки живут в другой спецификации, и имеют собственный инстанс -SVGElement
, в связи с чем, вся логика, основывающаяся на проверкеisHTMLElement
- проходила мимоSVG
, либо неправильно отрабатывала:getRootNode
на 58-ой строчке есть проверка:SVG
возвращалаnull
, хотя должна -rootNode
Popup
на 338-ой и 348-ой строчках задаются и очищаются слушатели событий, которые завязаны на проверкеisHTMLElement
Popup
на 573-ей строчке, возвращается расположение попапа, которое также завязано на проверкеisHTMLElement
Есть и другие кейсы, в которых старое определение
HTML
-элемента ломает приложение (вплоть до его краша), но они не относятся к этой задачеИз-за того, что различные инстансы для
HTMLElement
иSVGElement
обусловлены различными спецификациями, а также из-за того чтоHTML
-элементы иSVG
-иконки неразрывно связаны - было решено расширить проверку в изначальной функцииisHTMLElement
до:Поиски решения:
Как было сказано выше - если обернуть тег
<svg />
в любой другой тег, то проблема уйдёт, но на месте старой проблемы появится новая, в виде мёртвой зоны, в которой не будет отрабатывать хинт, либо в виде увеличившейся зоны хинта (вероятно зависит от иконки, я сильно не погружался, так как расцениваю этот вариант как хак)На скриншоте: тег
<svg />
был обёрнут в<span />
, из-за чего его нижняя граница увеличилась на 0.5 пикселей, возможен также случай, когда нижняя граница увеличивается и съезжает вниз, образуя мёртвую зонуТакже были и другие варианты, но для истории, пожалуй, важен только этот
P.S. Изначально для задачи были написаны интеграционные тесты (и отрефакторены старые), но по итогу оказалось, что
RTL
при клике с фиксом и без - рендерит текст хинта, из-за чего переключился на скриншотные тестыСсылки
IF-608
Чек-лист перед запросом ревью
any
(#2856)