Эта статья — перевод оригинальной статьи «Perfectly Pointed Tooltips: All Four Sides». Это вторая часть, первая часть уже есть на Хабре (Идеально размещённые тултипы: база), будет ещё перевод 3-ей части.
Также я веду телеграм канал «Frontend по‑флотски», где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
Пора перейти ко второй части! У нас уже есть очень хорошие функциональные тултипы с позиционированием, но они в основном «смотрели» вверх или вниз и смещались у краёв, чтобы избежать выхода за границы. Теперь мы пойдём дальше и рассмотрим четыре позиции без смещений.
На момент написания только Chrome и Edge полностью поддерживают возможности, которые мы будем использовать.
Вот демонстрация того, что мы создаём:

Перетащите якорь и посмотрите, как тултип переключается между четырьмя позициями и остаётся по центру относительно якоря.
Первичная настройка
Мы будем использовать ту же структуру кода, что и в первой части. Начнём с тултипа, расположенного над якорем (в позиции «top»).
<div id='anchor'></div>
<div id='tooltip'></div>
#anchor {
anchor-name: --anchor;
}
#tooltip {
--d: 1em; /* расстояние между тултипом и якорем */
position: absolute;
position-anchor: --anchor;
position-area: top;
bottom: var(--d);
}

Дальше всё будет отличаться от предыдущего примера.
Определение нескольких позиций
Свойство position-try-fallbacks позволяет задавать несколько позиций. Давайте попробуем следующее:
position-try-fallbacks: bottom, left, right;
Не забудем, что размещение связано с блоком-контейнером, который в нашем примере — это body (обозначено пунктирной рамкой):

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

Как и в первом примере, при переключении на резервные позиции промежуток исчезает. Мы знаем, как это исправить! Вместо явного задания позиций можно воспользоваться функцией «flip».
Чтобы переместить тултип с позиции «top» на «bottom», используем flip-block:
position-try-fallbacks: flip-block, left, right;
Чтобы переместить тултип с позиции «top» на «left», используем flip-start:
position-try-fallbacks: flip-block, flip-start, right;
Значение flip-block отражает позицию относительно горизонтальной оси, а flip-start делает то же самое по диагонали. С помощью этого значения можно перемещать тултип с «top» на «left» и с «bottom» на «right». Логично, что существует ещё flip-inline, который учитывает вертикальную ось и позволяет перемещать тултип с «left» на «right».
Но как же переместить тултип с позиции «top» на «right»? Нам не хватает ещё одного значения, верно?
Нет, все необходимые значения у нас уже есть. Чтобы переместить тултип с позиции «top» на «right», мы комбинируем два отражения: сначала flip-block, чтобы перейти вниз, а затем flip-start, чтобы сдвинуться вправо:
position-try-fallbacks: flip-block, flip-start, flip-block flip-start;
Или же сначала flip-start, чтобы сдвинуться влево, а затем flip-inline, чтобы перейти вправо:
position-try-fallbacks: flip-block, flip-start, flip-start flip-inline;

Стоит отметить, что все отражения (flips) учитывают исходную позицию, заданную для элемента, а не предыдущую позицию из position-try-fallbacks или текущую позицию. То есть, если сначала выполнить flip-block, чтобы переместить тултип вниз, то flip-start для второй позиции не будет учитывать нижнюю позицию, а будет исходить из верхней (начальной). Это может запутывать, особенно если позиций много.
Иными словами, браузер сначала преобразует все отражения в позиции (отталкиваясь от исходной позиции), а затем при необходимости выбирает подходящую.
Отключение смещения
То, что мы имеем сейчас, на самом деле неплохо и может идеально подойти для некоторых случаев, но мы стремимся к немного более продвинутой функциональности. Нам нужно, чтобы тултип сразу же переключался на левую или правую позицию, как только касается края. Мы не хотим поведения «смещения». Я хочу, чтобы тултип всегда оставался по центру относительно якоря.

Для этого мы можем использовать:
justify-self: unsafe anchor-center;
Что это за странное значение!?
После того как мы задали позицию элемента с помощью position-area, мы также можем управлять его выравниванием с помощью justify-self и align-self (или сокращённой записи place-self). Однако обычно значение по умолчанию подходит, и менять его редко приходится.
Для position-area: top выравнивание по умолчанию эквивалентно justify-self: anchor-center и align-self: end.
Разве у нас нет значения «center»? Почему оно называется
anchor-center?
Значение center существует, но его поведение отличается от anchor-center. Значение center учитывает центр самой области, а anchor-center — центр якоря по соответствующей оси.
Вот скриншот из моей интерактивной демо-версии, где видно эту разницу:

Кроме того, anchor-center следует логике безопасного выравнивания, что и вызывает поведение смещения. Если места для центрирования недостаточно, элемент сдвигается, чтобы оставаться внутри области контейнера. Чтобы отключить это, мы указываем браузеру использовать «небезопасное» поведение, отсюда и применение:
justify-self: unsafe anchor-center;
Вот демо с только верхней и нижней позициями. Обратите внимание, что тултип теперь выходит за границы слева и справа, вместо того чтобы смещаться.

А если мы снова добавим левую и правую позиции в резервные, браузер будет использовать их вместо выхода за границы!

Стоит отметить, что justify-self также учитывается при отражении (flip). Это одно из свойств, которое браузер изменяет при переключении позиции. Когда тултип находится в позициях top или bottom, оно остаётся justify-self, а когда в left или right — становится align-self. Ещё одна причина, почему лучше использовать функцию flip, а не задавать позиции явно.
Добавляем min-width
Позиция тултипа теперь корректна, но в некоторых случаях он получается слишком узким.

Это логичное поведение, так как текст внутри может переноситься, чтобы тултип поместился в выбранной позиции. Скорее всего, такое поведение вы захотите оставить, но в нашем случае мы хотим задать min-width, чтобы заставить тултип переключаться на другую позицию до того, как он слишком сожмётся. Также можно использовать max-height по аналогии.

Ой, min-width не предотвращает перенос текста, а вместо этого увеличивает высоту! Что?!
Можете догадаться, в чём проблема? Подумайте немного.
Дело в поведении flip.
Все свойства размеров, включая min-width, тоже подвержены отражению. Исходная конфигурация — top, поэтому задание min-width означает, что при выполнении flip-start для перехода на левую или правую позицию оно превращается в min-height, что, конечно, плохо.
Так что вместо этого задаём
min-height— при отражении оно превращается вmin-width!
Да, но тогда min-height будет применяться к верхней и нижней позициям, что тоже не идеально.
Исправить это можно с помощью кастомных позиций, где мы задаём все свойства вручную.
#tooltip {
min-width: 10em;
position-area: top;
justify-self: unsafe anchor-center;
bottom: var(--d);
position-try-fallbacks: flip-block,--left,--right;
}
@position-try --left {
position-area: left;
justify-self: normal;
align-self: unsafe anchor-center;
right: var(--d);
}
@position-try --right {
position-area: right;
justify-self: normal;
align-self: unsafe anchor-center;
left: var(--d);
}
Мы используем @position-try, чтобы создать кастомную позицию с заданным именем, и внутри неё определяем все свойства. Вместо использования flip-start для установки левой позиции, я создаю кастомную позицию --left с нужными свойствами, чтобы тултип корректно располагался слева. То же самое делаем для правой позиции. В этом случае min-width сохраняется для всех позиций, так как мы больше не используем flip-start.
Стоит отметить, что при использовании кастомной позиции необходимо убедиться, что вы переопределяете все свойства исходной позиции, заданной на элементе, иначе они всё равно будут применяться. По этой причине я задаю justify-self: normal, чтобы переопределить justify-self: unsafe anchor-center. Значение normal является значением по умолчанию для justify-self.

Хотя это решение работает, оно довольно многословное, поэтому мне стало интересно, можно ли сделать лучше. Оказывается, можно!
Мы можем комбинировать функцию flip и кастомные позиции, чтобы сократить код:
#tooltip {
position-area: top;
justify-self: unsafe anchor-center;
bottom: var(--d);
position-try: flip-block,--size flip-start,--size flip-start flip-inline;
}
@position-try --size {
min-height: 12em; /* Это будет min-width! */
}
Когда мы задаём кастомную позицию с flip, браузер сначала берёт свойства, определённые внутри кастомной позиции, а также свойства, уже заданные на элементе, и только потом выполняет отражение. То есть --size flip-start отразит свойства, заданные на элементе, и те, что определены в кастомной позиции --size. В результате min-height превращается в min-width! Умно, правда?
Но ты сказал, что нельзя использовать
min-height?
Мы не можем использовать его на основном элементе, так как оно применялось бы к верхней и нижней позициям. Однако внутри кастомной позиции я могу выбрать, где оно применяется, и мне нужно, чтобы оно действовало только для левой и правой позиции. При этом для верхней и нижней позиции никаких ограничений по min-width или min-height не требуется.

Теперь позиция нашего тултипа идеальна! Давайте добавим «хвостик».
Добавление хвостика
Сначала создаём форму, которая будет содержать все 4 хвостика.

#tooltip:before {
content: "";
position: absolute;
z-index: -1;
inset: calc(-1*var(--d));
clip-path: polygon(
calc(50% - var(--s)) var(--d),50% .2em,calc(50% + var(--s)) var(--d),
calc(100% - var(--d)) calc(50% - var(--s)), calc(100% - .2em) 50%,calc(100% - var(--d)) calc(50% + var(--s)),
calc(50% + var(--s)) calc(100% - var(--d)),50% calc(100% - .2em),calc(50% - var(--s)) calc(100% - var(--d)),
var(--d) calc(50% + var(--s)), .2em 50%,var(--d) calc(50% - var(--s))
);
}
Затем мы управляем хвостиком с помощью margin на элементе тултипа, так же как делали в первой части. Когда позиция — top, мы добавляем отступ со всех сторон, кроме нижней:
margin: var(--d);
margin-bottom: 0;

А для остальных сторон ничего делать не нужно! Отражение (flip) выполнит всю работу за нас.

Включите «режим отладки», чтобы увидеть, как форма ведёт себя в каждой позиции.
Заключение
Вторая часть завершена. Теперь вы должны уверенно работать с резервными позициями, функцией flip и кастомными позициями. Если что-то остаётся непонятным, перечитайте статью ещё раз. У нас впереди ещё одно финальное задание, поэтому убедитесь, что всё усвоено, прежде чем переходить к следующей статье.
Kerman
А так должно быть?