Головна / Статті / Заміна бібліотек підказок на API Popover та позиціонування елементів за допомогою CSS Anchor

Заміна бібліотек підказок на API Popover та позиціонування елементів за допомогою CSS Anchor

Чому потрібні підказки Popper та Floating UI — корисні вбудовані функції, які зараз вирішують проблеми складання елементів, позиціонування та закриття підказок, — і коли все ще варто використовувати бібліотеку JavaScript.

2090 слів

Показ текстової лінії поруч із кнопкою здається простою справою, проте існують Popper.js, Floating UI та безліч додаткових пакетів саме через те, що це ніколи не було можливим. Підказка насправді є трьома окремими проблемами, які накладаються одна на одну, і до нещодавнього часу платформа не пропонувала жодного ефективного рішення для жодної з них. У цьому посібнику проблеми розбираються окремо, показується, скільки місця займає кожна з них у готовому JavaScript-коді, та вони прив’язуються до двох функцій браузера, які тепер вирішують поширені ситуації: API Popover та CSS Anchor Positioning. До кінця ви дізнаєтесь, які частини бібліотеки підказок можна видалити, а які все ще можуть знадобитися.

Три проблеми, приховані всередині однієї підказки

Кожна з цих проблем є давньою та добре відомою обмеженістю у поведінці браузера до появи нових специфікацій:

  • Розташування. Чи буде підказка відображатися над усім іншим, чи її обмежить значення overflow: hidden у якогось предка?
  • Позиціонування. Чи знає підказка, де на екрані знаходиться елемент-тригер, і чи слідує вона за цією позицією під час прокручування та зміни розмірів?
  • Закриття. Чи закривається вона автоматично, коли користувач клацає в іншому місці чи натискає клавішу Escape?

Протягом більшої частини історії Інтернету кожен проект, якому потрібна була підказка, спадний список чи список автодоповнення, вирішував усі три проблеми за допомогою JavaScript. Рішення для кожної з них різні, і саме через спробу розглядати їх як одну проблему невелика деталь інтерфейсу перетворилася на залежність. Тому варто розглядати їх по окремо.

Розташування: чому z-index не може вийти за межі свого контексту

Кожен елемент належить до контексту накладання, який визначає, що буде намальовано поверх чого. z-index впорядковує елементи всередині одного контексту накладання; він не може вивести елемент за межі контексту, до якого той належить.

Розмістіть підказку всередину контейнера з параметром overflow: hidden або всередину модального вікна, яке створює власний контекст накладання, і жодне значення, навіть 999999, не дозволить їй з’явитися над цими межами. Підказка обмежується правилами відображення її предків, і z-index не має впливу за їхніми межами.

Традиційним способом вирішення цієї проблеми був портал: розміщували маркап типового підказки в іншій частині документа, зазвичай після <body>, щоб він більше не успадковував параметри обрізки та викладення від батьківського елемента. Функція createPortal у React існує саме через це. Це скоріше обхідний захід для того, чого CSS не міг реалізувати, ніж справжня функція React.

Позиціонування: абсолютне позиціонування розуміє лише предків

position: absolute розміщує елемент відносно його найближчого позиціонованого предка, тобто найближчого елемента у дереві, чия властивість position дорівнює relative, absolute, fixed або sticky. Ключовим поняттям є „предок“: елемент-орієнтир має знаходитися в тій самій гілці DOM, вище за позицією типової підказки.

Як тільки підказка та її тригер стають братами-сестрами, або підказку переміщують у <body> для вирішення проблеми накладання елементів, тригер більше не є предком. У CSS не було способу сказати «розташуй цей елемент відносно того неспорідненого елемента». Проблемою було не саме позиціонування в CSS загалом, а відсутність будь-яких механізмів позиціонування, які б ігнорували структуру документа.

Бібліотеки заповнили цю прогалину за допомогою вимірювань. Вони використовують getBoundingClientRect(), щоб отримати координати області відображення тригера, розрахувати координати підказки та повторювати ці розрахунки під час кожного прокручування та зміни розмірів, оскільки числа постійно змінюються. Цей безперервний цикл вимірювання та розміщення є основною функцією бібліотек позиціонування під час їх роботи.

Відкликання: поведінка, яку неможливо було описати за допомогою маркапу

До появи API Popover у HTML та CSS не існувало можливості „закриття при кліку користувача за межами інформаційного вікна або натисканні клавіші Escape“. Усе це реалізувалося за допомогою скриптів: слухач подій на document, який перевіряв, чи знаходиться об’єкт події за межами інформаційного вікна, слухач подій keydown, який чекав на натискання клавіші Escape, а також код для очищення обох механізмів під час видалення компонента, щоб уникнути витоків ресурсів. На відміну від перших двох проблем, ця не має геометричного аспекту – це суто поведінкова проблема, але все одно це ще один фрагмент коду, який браузер не надавав.

Popovers проти модалних вікон

Це допомагає чітко визначити термінологію перед тим, як розглядати синтаксис. Popover — це все, що відображається над рештою сторінки, розташовується відносно тригера та закривається при кліку ззовні або натисканні клавіші Escape. Підказки, меню з випаданням, списки автодоповнення та контекстні меню — це всі popover з різним оформленням, але з тими самими трьома проблемами.

Modal також має проблему чергування елементів, але не є popover, оскільки блокує інші елементи. Поки modal відкритий, контент позаду нього є неактивним: користувачі не можуть переходити на нього, клікати або прокручувати його, і зазвичай є фоновий елемент, який залишається активним всередині modal до його закриття. Уявіть собі запит на підтвердження видалення: ніщо інше на сторінці не може використовуватися, поки на нього не буде відповідь. Popover нічого не блокує; сторінка залишається повністю інтерактивною, а popover просто закривається, коли користувач переходить далі.

Специфікації прямо визначають це розрізнення:

  • popover="auto" дозволяє легко закрити поп-ап без блокування: він закривається при кліку за межі або натисканні клавіші Escape, звільняючи фокус. До цієї категорії належать підказки та випадаючі списки.
  • popover="manual" залишається відкритим доки ваш скрипт його не закриє, без можливості легкого закриття, що підходить для постійних сповіщень у вигляді тостів.
  • <dialog>, відкритий за допомогою .showModal(), є версією з блокуванням: він опиняється на верхньому рівні, має фон, утримує фокус та робить все, що знаходиться позаду, недієздатним.
  • Відкриття цього <dialog> за допомогою .show() дає елемент без блокування, який поводиться майже як поп-ап.

Які є недоліки підходу з JavaScript

Наведені нижче розміри після стиснення взяті з npm та стосуються лише самих бібліотек:

popper.js (v1, now deprecated)              7.1 KB
Tippy.js (bundles @popperjs/core)          14.1 KB
Floating UI, vanilla (@floating-ui/dom)     8.1 KB
Floating UI, React bindings                30.1 KB
react-tooltip (@floating-ui/dom + clsx)    14.1 KB

Ці цифри не враховують усе, що додається зверху: конфігурацію, компонент-обгортку, CSS для стрілок та тем. Це базова вартість до додавання будь-якої власної логіки, а проект, який поєднує, скажімо, пакет підказок із окремим пакетом випадаючих меню, оплачує її двічі.

Жоден з цих факторів не свідчить про погану інженерну реалізацію. Особливо ретельно побудоване є UI у вигляді плаваючих елементів; його основне завдання — правильно працювати з перекиданням елементів при переповненні у всіх браузерах, незалежно від їхніх особливостей. Витрати виникли через необхідність одночасного вирішення трьох незалежних проблем у JavaScript, оскільки платформа не пропонувала нічого іншого.

API Popover керує відображенням та приховуванням

Бібліотеку замінили двома окремими специфікаціями, які не розділяють обов’язки так, як можна було б очікувати. Атрибут popover одночасно керує відображенням та приховуванням, майже без використання скриптів.

<button popovertarget="my-tooltip">Hover me</button>
<div id="my-tooltip" popover="auto">
  This is the tooltip content.
</div>

Атрибут popovertarget під’єднує кнопку до елемента з відповідним id. За допомогою popover="auto" елемент переміщується на верхній рівень браузера під час його відкриття, що саме є способом уникнення ефекту overflow: hidden та контекстів накладання, описаних раніше; крім того, його можна легко закрити: кліки за межами та натискання клавіші Escape закривають його без необхідності коду-приймача.

Одна корекція формулювання в маркапі: popovertarget увімкнюється під час активації, тобто після кліку, натискання або натиснення на клавіатурі, а не під час наведення курсора. Справжній підказковий інструмент для наведення все ще потребує невеликої кількості коду для виклику функцій showPopover() та hidePopover() під час подій курсора та фокусу, або новішого декларативного механізму, як тільки це стане можливим у підтримуваних браузерах. Навіть у такому разі для створення шарування та закриття елементів більше не потрібні бібліотеки. Залишається проблема позиціонування, яка належить до іншої специфікації.

Позиціонування анкорів — посилання елементів за іменем

CSS Anchor Positioning вирішує єдину проблему, яку залишає без уваги API Popover. Він дозволяє будь-яким двом елементам у документі посилатися один на одного за іменем, а не через структуру батьків та дочок.

.trigger {
  anchor-name: --my-anchor;
}

.tooltip {
  position: absolute;
  position-anchor: --my-anchor;
  top: anchor(--my-anchor bottom);
  left: anchor(--my-anchor left);
}

anchor-name реєструє тригер під дефісованим ідентифікатором, який використовується у користувацьких властивостях. position-anchor у підказці вказує на цю назву, а функція anchor() зчитує певну грань анкора (top, right, bottom, left або center), щоб підказка могла вирівнятися за нею.

Важливо те, що один елемент не обов’язково має містити інший. Браузер тепер автоматично виконує те, що раніше доводилося обчислювати вручну за допомогою getBoundingClientRect() на кожному кроці прокрутки. Якщо поєднувати це з поп-апом, пам’ятайте, що стилі користувацького агента надають елементам [popover] значення inset: 0 та margin: auto для їх центрування; якщо підказка ігнорує ваші зсуви анкора, зазвичай рішенням є скидання цих властивостей.

Увімкнення функції обробки переповнення без слухача прокрутки

Частиною бібліотеки позиціонування, яка містить більшу частину її логіки, є обробка переповнення: виявлення того, що підказка ось-ось вийде за межі видимої області, та вибір іншого місця розміщення спочатку. Функція позиціонування за анкором вирішує цю проблему за допомогою position-try-fallbacks.

.tooltip {
  position: absolute;
  position-anchor: --my-anchor;
  position-area: top center;
  position-try-fallbacks: flip-block, flip-inline;
}

Тут position-area: top center встановлює стандартне місце розміщення, а position-try-fallbacks перелічує альтернативи, які браузер пробує у порядку, коли це місце може призвести до переповнення свого контейнерного блоку чи видимої області. flip-block віддзеркалює підказку по осі блоку, тож верхній край стає нижнім, а flip-inline — по осі вирівнювання, тож лівий край стає правим. Браузер переоцінює це під час формування макету, без слухача прокрутки та без скрипту на основній смузі, який би виявляв переповнення.

Коли простого дзеркала недостатньо, правило @position-try дозволяє визначити іменовані альтернативні місця розташування — кожне з яких є невеликим блоком оголошень про позиціонування, які браузер може по черзі використовувати.

@position-try --below {
  position-area: bottom center;
  margin-top: 8px;
}

@position-try --above {
  position-area: top center;
  margin-bottom: 8px;
}

.tooltip {
  position-anchor: --my-anchor;
  position-try-fallbacks: --above, --below;
}

Це та сама стратегія, яку використовує Floating UI у JavaScript під час кожної події прокрутки, оголошена заздалегідь як дані, які обробляє двигун макетування. Зауважте, що правило .tooltip у цьому фрагменті передбачає, що елемент вже має абсолютне чи фіксоване позиціонування, як у попередніх прикладах; позиціонування за допомогою анкора не впливає на елементи зі статичним позиціонуванням.

Коли все ще доцільна бібліотека позиціонування

Для інструментів типу підказок, простих списків з випаданням чи списків автодоповнення тепер розумним стандартом є використання вбудованих можливостей. Роль бібліотеки скоротилась, хоча й не зникла повністю.

Підтримка браузерами є першою обмеженням. Браузери Chromium підтримують позиціонування елементів anchor з версії 125, а @position-try досяг рівня Baseline пізніше, ніж сам anchor(). Підтримка у Safari та Firefox з’явилась пізніше, і цифри, наведені в Інтернеті, різняться, тому краще перевіряти актуальну таблицю сумісності, а не покладатися на якийсь один список версій. У випадках відсутності підтримки не існує грайливого зниження функціоналу лише за допомогою CSS: браузер, який не розуміє anchor(), просто не може правильно розташувати елемент. Якщо вам необхідно підтримувати старі версії Safari чи мобільні браузери зі старими двигунами, тримайте запасний варіант; наш посібник з безпечної реалізації сучасного CSS охоплює виявлення функцій та поступове покращення роботи елементів anchor.

Другий випадок – це правила розміщення, які виходять за межі простого відображення при переповненні: плаваюча панель, що містить віртуалізований список, перевірка зіткнень із кількома межами одночасно, або розміщення, зумовлене даними програми, а не її макетом. Скрипт може реагувати на будь-який стан програми, тоді як фіксований CSS-фолбек лише враховує макет.

У звичайному випадку, який стосується більшості проектів, використання від 8 до 30 КБ JavaScript для виконання роботи, яку зараз сам виконує браузер, є важким для обґрунтування.

Ключові висновки

  • Інструмент підказок стикається з трьома проблемами: відступлення елементів, позиціонування та закриття. Бібліотеки існують тому, що всі ці проблеми потрібно було вирішувати за допомогою скриптів.
  • API Popover вирішує проблему накладення елементів через використання верхнього шару та закриття через легкий сигнал; параметри popover="manual" та разом із методом .showModal() покривають випадки постійного відображення та блокування інтерфейсу.
  • CSS Anchor Positioning дозволяє визначати положення елементів без наявності батьківських елементів, а параметри position-try-fallbacks та @position-try замінюють логіку обробки переповнення, яка домінувала у бібліотеках раніше.
  • Поведінка елементів під час наведення курсору та підтримка браузерами — це дві проблеми, які потрібно враховувати; перевіряйте дані про сумісність для вашої аудиторії перед видаленням бібліотеки.
  • Використовуйте Floating UI там, де потрібна підтримка старих браузерів або позиціонування на основі даних, а в усіх інших випадках краще використовувати стандартні можливості платформи. Самі бібліотеки не стали гіршими; браузер нарешті взяв на себе завдання, яке протягом понад десяти років залишалося за JavaScript.