Смежные селекторы предшествующих элементов CSS и способы их имитации

Смежные селекторы предшествующих элементов CSS и способы их имитации

От автора: если вы когда-либо использовали смежные селекторы CSS одного уровня, вы знаете, что их всего два. Комбинатор + выбирает первое соответствие, которое находит сразу после, а комбинатор ~ выбирает все последующие.

Но у нас нет способа выбрать те, которые расположены раньше. Либо родительские селекторы, либо предыдущие селекторные одного уровня — такого просто нет.

Я знаю, что вы этого хотите, вы знаете, что я этого хочу, но суровая правда в том, что они не существуют (и, вероятно, никогда не будут). Есть миллион сообщений «Почему?». Есть даже предложения о том, как их реализовать. Но мы застреваем на однонаправленной обработке правил CSS — это делается, скорее всего, чтобы защитить нас от «плохого опыта», связанного с неправильным течением потока обработки или даже бесконечными циклами.

К счастью, как и в случае большинства ограничений CSS, мы можем имитировать это. Первое, что нужно учитывать — это то, для чего нам нужны предыдущие одноуровневые элементы. Существуют два случая:

Нам нужно выбрать все элементы одного уровня с элементом, а комбинатор ~ выбирает только те, которые расположены после.

Нам нужно выбрать только элементы одного уровня, которые расположены раньше

1. Выбор всех элементов одного уровня

Иногда нам нужно выбрать как предыдущие, так и последующие элементы одного уровня. Для этого мы можем выбрать родительский элемент и использовать некоторые хитрости.

Например, чтобы выбрать все span в следующей структуре, когда мы наводим указатель мыши на любой из них, мы могли бы просто использовать селектор дочерних элементов для родителя. Нам нужно переключить pointer-events с родителя на дочерние элементы. Таким образом, любое действие, которое мы хотим осуществить, будет срабатывать только при вводе для дочернего элемента, а не самого родителя.

<div class="container"> <span> 1st element </span> <span> 2nd element </span> <span> 3rd element </span>
</div>

.container{ pointer-events:none
} .container > span{
pointer-events: auto;
}
.container:hover > span{ background: red; /*или что угодно, что вы хотите сделать с со всеми элементами одного уровня, когда вы наводите мышь на один из них */
}

Если вам нужно выбрать все элементы одного уровня, кроме того, на который был наведен курсор, вы можете совместить предыдущий метод с селектором :not, чтобы исключить его. Стандартным примером для этого является меню:

<ul> <li>first item</li> <li>second item</li> <li>third item</li>
</ul>

ul:hover > li:not(:hover){ opacity: 0.5;
}

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

Кроме того, вы можете использовать селекторы type и nth, чтобы дополнительно отфильтровать нужные элементы. Задав определенные стили, мы можем получить следующее:

Обратите внимание: если вы хотите использовать подход pointer-events:none, помните, что это может нарушить порядок стекирования (возможно, вы выберете элементы, которые «ниже» в порядке стекирования). Это также не будет работать в IE10 и ниже, если только вам не нужны указатели события для чего-то другого. Поэтому будьте очень осторожны при использовании.

2. Выбор предыдущих элементов

Для этого варианта использования мы можем изменить в обратном направлении порядок сортировки в HTML, а затем отсортировать элементы обратно в CSS и использовать селекторы ~ и +. Таким образом, мы будем выбирать следующие элементы, но самом деле это будут предыдущие.

Существует несколько способов сделать это. Самый простой и, вероятно, самый старый — это изменение направления написания нашего контейнера:

<div class="container"> <span> 3rd element </span> <span> 2nd element </span> <span> 1st element </span>
</div>

.container{ direction: rtl; /* встроенные элементы будут размещаться справа на лево */
}

Если ваши элементы должны отображать фактический текст, вы всегда можете отменить это обратно:

.container > span { direction: ltr;
}

Но во многих случаях это может быть неудобно. К счастью, современный инструментарий CSS позволяет сделать это намного проще и безопаснее. Мы можем просто использовать для контейнера Flexbox и изменить порядок с помощью flex-direction: row-reverse:

.container{ display:flex; flex-direction: row-reverse;
}

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

Использование «предыдущих элементов одного уровня» для создания системы звезд-рейтингов на чистом CSS

Семантически, рейтинговую систему можно рассматривать как простой список радио-кнопок с соответствующими метками. Это позволит нам использовать проверенный псевдо-селектор :checked для изменения элементов одного уровня. Итак, давайте начнем с этого:

<div class="rating"> <input type="radio" name="rating" value="5" id="5"><label for="5">☆</label> <input type="radio" name="rating" value="4" id="4"><label for="4">☆</label> <input type="radio" name="rating" value="3" id="3"><label for="3">☆</label> <input type="radio" name="rating" value="2" id="2"><label for="2">☆</label> <input type="radio" name="rating" value="1" id="1"><label for="1">☆</label>
</div>

Как мы рассматривали ранее, элементы находятся в обратном порядке тому, который позволяет нам применить селектор «предыдущий элемент одного уровня». Обратите внимание, что мы используем для представления пустых звезд символ юникода “white star” (U + 2606).

Давайте отобразим их рядом друг с другом, в правильном (обратном) порядке:

.rating { display: flex; flex-direction: row-reverse;
}

Теперь скроем сами переключатели, никому не нужно их видеть:

.rating > input{ display:none;
}

И применим некоторые стили к символам звездочек:

.rating > label { position: relative; width: 1.1em; font-size: 15vw; color: #FFD700; cursor: pointer;
}

Единственная действительно важная строка — это position: relative. Это позволит нам по абсолютно позиционировать псевдо-элемент с закрашенной звездой (U + 2605), который будет изначально скрыт.

.rating > label::before{ content: "\2605"; position: absolute; opacity: 0;
}

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

.rating > label:hover:before,
.rating > label:hover ~ label:before { opacity: 1 !important; /* shame... shame... shame... I'll explain that later*/
}

То же делается для выбранного рейтинга, через сопоставление всех меток, которые находятся перед выбранной радио-кнопкой:

.rating > input:checked ~ label:before{ opacity: 1;
}

Помните, что использование флага !important прямо противоположно хорошей практике. Я делаю это здесь, так как другого способа реализовать данный функционал нет.

И последнее, но не менее важное: нам нужно «запомнить» текущий рейтинг, на случай, если пользователь захочет его изменить. Например, если он выбрал пять звезд и по какой-либо причине хочет изменить их на четыре, мы должны отображать звезды с 1 по 4, как заполненные, а пятую — полупрозрачной при наведении указателя на четвертую.

Это может быть достигнуто путем изменения при наведении курсора на контейнер непрозрачности предыдущих элементов одного уровня выбранного ввода:

.rating:hover > input:checked ~ label:before{ opacity: 0.4;
}

Вот для чего нам нужно pacity:1 !important в начальном объявлении наведения. В противном случае это последнее правило переопределило бы другие в соответствии со специфичностью и применило полупрозрачную заливку ко всему.

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

Как вы можете видеть, просто потому, что «это невозможно» не означает, что вы не должны пытаться. Программирование — это раздвижение границ. Поэтому каждый раз, когда вы упираетесь в стену, просто попробуйте надавить немного сильнее. Или лучшей аналогией, я думаю, будет — найдите свой обходной путь? … в любом случае, вы понимаете, что я имею в виду. Продолжайте взламывать!

Замечания относительно доступности

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

Чтобы сделать сниппет немного более доступным, первым делом нужно было бы скрыть радио-кнопки с помощью другого способа, а не display: none, чтобы сделать их фокусируемыми. Мы также должны добавить кольцо фокусировки на весь фрагмент звёзд, когда любой элемент внутри выделен фокусом, это делается через псевдо-селектор :focus-in.

Метки «звезды» не имеют смысла для экранных дикторов, поэтому наилучшим подходом будет наличие внутри метки span с текстом «n Stars», который будет скрыт от видящих пользователей.

Кроме того, подход с обратным порядком в HTML-источнике + display:row-reverse делает клавиатуру неудобной для выставления рейтингов. Доступность Flexbox и клавиатуры — довольно сложная тема, но ближе всего к решению подход, когда для каждого элемента добавляется тег aria-flowtotag, что, по крайней мере, устраняет проблему для некоторых экранных дикторов + комбинации браузеров.

Более доступный сниппет (использующий альтернативную технику изменения следующих элементов одного уровня, которые выглядят пустыми) вы можете найти у Патрика Коула, как описано в комментариях ниже.

Автор: Facundo Corradini

Источник: https://medium.freecodecamp.org/

Редакция: Команда webformyself.