From 8c89d4d451ada0d299548ca3f91da29f573a04f9 Mon Sep 17 00:00:00 2001 From: Alena Batitskaia Date: Wed, 11 Dec 2024 19:58:35 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D1=82=20=D1=80=D0=B5=D1=86=D0=B5=D0=BF=D1=82=20=D1=80?= =?UTF-8?q?=D0=B5=D0=B9=D1=82=D0=B8=D0=BD=D0=B3=D0=B0=205=20=E2=AD=90?= =?UTF-8?q?=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- recipes/star-rating/demos/css-1/index.html | 96 ++++ recipes/star-rating/demos/css-2/index.html | 101 ++++ recipes/star-rating/demos/css-3/index.html | 102 ++++ recipes/star-rating/demos/html/index.html | 33 ++ recipes/star-rating/demos/js-1/index.html | 116 +++++ recipes/star-rating/demos/js-2/index.html | 121 +++++ recipes/star-rating/demos/js-3/index.html | 125 +++++ recipes/star-rating/demos/js-4/index.html | 127 +++++ recipes/star-rating/demos/js-css/index.html | 131 ++++++ recipes/star-rating/demos/result/index.html | 130 ++++++ recipes/star-rating/index.md | 492 ++++++++++++++++++++ 11 files changed, 1574 insertions(+) create mode 100644 recipes/star-rating/demos/css-1/index.html create mode 100644 recipes/star-rating/demos/css-2/index.html create mode 100644 recipes/star-rating/demos/css-3/index.html create mode 100644 recipes/star-rating/demos/html/index.html create mode 100644 recipes/star-rating/demos/js-1/index.html create mode 100644 recipes/star-rating/demos/js-2/index.html create mode 100644 recipes/star-rating/demos/js-3/index.html create mode 100644 recipes/star-rating/demos/js-4/index.html create mode 100644 recipes/star-rating/demos/js-css/index.html create mode 100644 recipes/star-rating/demos/result/index.html create mode 100644 recipes/star-rating/index.md diff --git a/recipes/star-rating/demos/css-1/index.html b/recipes/star-rating/demos/css-1/index.html new file mode 100644 index 0000000000..6da2758e81 --- /dev/null +++ b/recipes/star-rating/demos/css-1/index.html @@ -0,0 +1,96 @@ + + + + Базовые стили — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + diff --git a/recipes/star-rating/demos/css-2/index.html b/recipes/star-rating/demos/css-2/index.html new file mode 100644 index 0000000000..974d1201ed --- /dev/null +++ b/recipes/star-rating/demos/css-2/index.html @@ -0,0 +1,101 @@ + + + + Стили при наведении курсора — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + diff --git a/recipes/star-rating/demos/css-3/index.html b/recipes/star-rating/demos/css-3/index.html new file mode 100644 index 0000000000..3fc23b792a --- /dev/null +++ b/recipes/star-rating/demos/css-3/index.html @@ -0,0 +1,102 @@ + + + + Перевёрнутый рейтинг — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + diff --git a/recipes/star-rating/demos/html/index.html b/recipes/star-rating/demos/html/index.html new file mode 100644 index 0000000000..7f96e67631 --- /dev/null +++ b/recipes/star-rating/demos/html/index.html @@ -0,0 +1,33 @@ + + + + HTML-разметка — Рейтинг «5 звёзд» — Дока + + + + + + + +
+ Оцените товар: + +
+ + diff --git a/recipes/star-rating/demos/js-1/index.html b/recipes/star-rating/demos/js-1/index.html new file mode 100644 index 0000000000..9f763350f0 --- /dev/null +++ b/recipes/star-rating/demos/js-1/index.html @@ -0,0 +1,116 @@ + + + + Обработка клика без стилей — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + + + diff --git a/recipes/star-rating/demos/js-2/index.html b/recipes/star-rating/demos/js-2/index.html new file mode 100644 index 0000000000..51d6a47c7e --- /dev/null +++ b/recipes/star-rating/demos/js-2/index.html @@ -0,0 +1,121 @@ + + + + Обработка клика со стилями — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + + + diff --git a/recipes/star-rating/demos/js-3/index.html b/recipes/star-rating/demos/js-3/index.html new file mode 100644 index 0000000000..b9564de8d5 --- /dev/null +++ b/recipes/star-rating/demos/js-3/index.html @@ -0,0 +1,125 @@ + + + + Обработка клика в обе стороны — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + + + diff --git a/recipes/star-rating/demos/js-4/index.html b/recipes/star-rating/demos/js-4/index.html new file mode 100644 index 0000000000..f06babe967 --- /dev/null +++ b/recipes/star-rating/demos/js-4/index.html @@ -0,0 +1,127 @@ + + + + Сохранение рейтинга в дата-атрибут — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + + + diff --git a/recipes/star-rating/demos/js-css/index.html b/recipes/star-rating/demos/js-css/index.html new file mode 100644 index 0000000000..e735d94a59 --- /dev/null +++ b/recipes/star-rating/demos/js-css/index.html @@ -0,0 +1,131 @@ + + + + Ховер при выбраном рейтинге — Рейтинг «5 звёзд» — Дока + + + + + + + + + +
+ Оцените товар: + +
+ + + + diff --git a/recipes/star-rating/demos/result/index.html b/recipes/star-rating/demos/result/index.html new file mode 100644 index 0000000000..eedbb59eb7 --- /dev/null +++ b/recipes/star-rating/demos/result/index.html @@ -0,0 +1,130 @@ + + + + Финальное решение — Рейтинг «5 звёзд» — Дока + + + + + + + + +
+ Оцените товар: + +
+ + + + diff --git a/recipes/star-rating/index.md b/recipes/star-rating/index.md new file mode 100644 index 0000000000..aa40b5e388 --- /dev/null +++ b/recipes/star-rating/index.md @@ -0,0 +1,492 @@ +--- +title: "Рейтинг «5 звёзд»" +description: "Популярный элемент для оценки по пятибальной шкале на чистом HTML, CSS и JavaScript." +authors: + - solarrust +tags: + - article +--- + +## Задача + +Создать компонент, состоящий из 5 звезд. На любую из звёзд можно кликнуть и все звёзды до неё и она сама станут отмеченными, активными. Если кликнуть на эту звезду второй раз, то все звёзды снова станут неактивными. По наведению курсора на любую звезду все элементы до неё и она сама становятся активными. При этом когда курсор убирается кликнутые звёзды остаются активными. + +## Готовое решение + + + +Разметка: + +```html +
+ Оцените товар: + +
+``` + +Стили: + +```css +button { + background-color: transparent; + border: none; + color: inherit; + cursor: pointer; + font: inherit; + padding: 0; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + clip: rect(0 0 0 0); + overflow: hidden; +} + +.star-rating { + text-align: center; + border: none; +} + +.stars { + list-style: none; + display: flex; + flex-direction: row-reverse; + gap: 1rem; + font-size: 5rem; +} + +.star { + opacity: 0.5; + cursor: pointer; + transition: opacity 0.2s; +} + +.stars:hover .star { + opacity: 0.5; +} + +.active, +.active ~ .star { + opacity: 1; +} + +.stars .star:hover, +.stars .star:hover ~ .star { + opacity: 1; +} +``` + +JavaScript: + +```javascript +const rating = document.querySelector('.star-rating') +const stars = document.querySelectorAll('.star') + +stars.forEach((star, index) => { + star.addEventListener('click', (event) => { + const activeStar = event.currentTarget + + if (activeStar.classList.contains('active')) { + activeStar.classList.remove('active') + rating.dataset.ratingValue = '' + } else { + stars.forEach(star => star.classList.remove('active')) + activeStar.classList.add('active') + rating.dataset.ratingValue = parseInt(activeStar.textContent.trim()) + } + }) +}) +``` + +## HTML + +В первую очередь создадим HTML-разметку для рейтинга. Элементы связаны по смыслу, поэтому нужно использовать подходящий семантический тег. Также для нативной поддержки фокуса используем тег [` + +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • + +``` + +Чтобы скринридеры воспринимали элемент целиком и зачитывали его подпись используем связку [`
    `](/html/fieldset/) + [``](/html/legend/). + + +```html +
    + Оцените товар: +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +``` + +Не забудем добавить скрытый [``](/html/span/) с классом [`visually-hidden`](/a11y/content-hidden/#klassy-.visually-hidden-.sr-only-.off-screen). Всему элементу добавим роль [`radiogrup`](/a11y/role-radiogroup/) и будем сохранять текущее значение рейтинга в [дата-атрибут](/html/data-attributes/) `data-rating-value`. Если никакая из звёзд не вбрана значение этого атрибута будет пустым. + +```html +
    + Оцените товар: +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +``` + + + +Разметка готова, можно переходить к стилям. + +## CSS + +В стилях будет своя «изюминка», но начнём с простого. + +Уберём рамку у `
    `, сбросим стандартные стили кнопок, сбросим маркеры у списка. Cкроем текст, предназначенный для скринридеров. Поставим звёзды в ряд, зададим между ними отступ и сделаем их большими. + +```css +button { + background-color: transparent; + border: none; + color: inherit; + cursor: pointer; + font: inherit; + padding: 0; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + clip: rect(0 0 0 0); + overflow: hidden; +} + +.star-rating { + text-align: center; + border: none; +} + +.stars { + list-style: none; + font-size: 5rem; + display: flex; + gap: 1rem; +} +``` + + + +Теперь сделаем каждую звезду полупрозрачной при помощи [`opacity`](/css/opacity/) и предусмотрим плавное изменение видимости при помощи свойства [`transition`](/css/transition/). + +```css +.star { + opacity: 0.5; + cursor: pointer; + transition: opacity 0.2s; +} +``` + +При наведении курсора на какую-то из звёзд будем менять её видимость (свойство `opacity`) и видимость всех звёдз, стоящих после неё в разметке, с 0.5 на 1. Потребуется [оператор `~`](/css/combined-selectors/#posleduyushchie-.element1-~-.element2) чтобы выбирать все звёзды, следующие в разметке **после** той, на которую наведён курсор. И про саму звезду, на которую наведён курсор тоже не забудем. [Перечислим](/css/combined-selectors/#gruppirovka-.element1-.element2) эти селекторы через запятую. + +```css +.star:hover, +.star:hover ~ .star { + opacity: 1; +} +``` + + + +Но пока всё работает непривычно, наоборот. Звёзды закрашиваются справа налево, а должны слева направо. Для этого в разметке кнопки поставлены в обратном порядке. Чтобы их можно было перевернуть и получить верный порядок. Для поворота используем свойство [`flex-direction`](/css/flex-direction/) со значением `row-reverse`. + +```css +.stars { + flex-direction: row-reverse; +} +``` + + +## JavaScript + +Теперь нужно реализовать нажатие на звезду. Для этого используем JavaScript. + +Найдём и сохраним в [константы](/js/const/) элемент, оборачивающий весь рейтинг и каждую из звёзд. Для этого используем [`.querySelector`](/js/query-selector/) и [`.querySelectorAll`](/js/query-selector-all/) соответственно. + +Дальше при помощи метода [`forEach()`](/js/array-foreach/) переберём массив со звёздами и каждой из них добавим слушатель события `click` при помощи метода [`addEventListener`](/js/events/#metod-addeventlistener). + +При клике на любую из звёзд запишем её в константу `activeStar`. Пройдёмся по всем звёздам ещё раз чтобы удалить класс `active` с любой из них, где бы он не стоял. Дальше той звезде, по которой кликнули, добавим класс `active`. + +```javascript +const rating = document.querySelector('.star-rating') +const stars = document.querySelectorAll('.star') + +stars.forEach((star, index) => { + star.addEventListener('click', (event) => { + const activeStar = event.currentTarget + + stars.forEach(star => star.classList.remove('active')) + activeStar.classList.add('active') + }) +}) +``` + + + +Если открыть инструменты разработчика для демо выше, то можно увидеть, что при клике на звёздочку у неё появляется класс `active`, но внешне она не меняется. Добавим стили для этого класса и для всех звёзд, стоящих после активной. + +```css +.active, +.active ~ .star { + opacity: 1; +} +``` + +Теперь при клике все звёзды до и та, по которой был клик становятся яркими. + + + +Теперь сделаем так, чтобы при повторном клике оценка убиралась и все звёзды снова становились неактивными. Для этого добавим проверку, есть ли класс `active` у звезды на которую кликнули или нет. + +Если есть, то убираем его, если нет, то добавляем. + +```javascript +const rating = document.querySelector('.star-rating') +const stars = document.querySelectorAll('.star') + +stars.forEach((star, index) => { + star.addEventListener('click', (event) => { + const activeStar = event.currentTarget + + if (activeStar.classList.contains('active')) { + activeStar.classList.remove('active') + } else { + stars.forEach(star => star.classList.remove('active')) + activeStar.classList.add('active') + } + }) +}) +``` + + + +Добавим пару строк чтобы менялось значение атрибута `data-rating-value` в зависимости от того, какая звезда выбрана. Для этого используем свойство [`.dataset`](/js/element-dataset/) и будем _читать_ значение скрытого текста, доставать оттуда цифру при помощи [`parseInt()`](/js/parseint/). + +```javascript +const rating = document.querySelector('.star-rating') +const stars = document.querySelectorAll('.star') + +stars.forEach((star, index) => { + star.addEventListener('click', (event) => { + const activeStar = event.currentTarget + + if (activeStar.classList.contains('active')) { + activeStar.classList.remove('active') + rating.dataset.ratingValue = '' + } else { + stars.forEach(star => star.classList.remove('active')) + activeStar.classList.add('active') + rating.dataset.ratingValue = parseInt(activeStar.textContent.trim()) + } + }) +}) +``` + + + +## Ещё немного CSS + +Осталось реализовать последнее требование. По наведению курсора на любую звезду все элементы до неё и она сама становятся активными. Это условие должно работать вне зависимости от того, какой рейтинг сейчас выбран. + +Для этого немного схитрим и будем делать полупрозрачными все звёзды если курсор находится в пределах родительского блока `.stars`. + +```css +.stars:hover .star { + opacity: 0.5; +} +``` + +Теперь доработаем правило для [`:hover`](/css/hover/), которе уже было в CSS. Увеличим вес селектора при помощи класса `.stars` в начале и обязательно подвинем его вниз, под предыдущее правило. Это нужно из-за [принципа каскада](/css/cascade/). Иначе правила начнут перебивать друг друга. + +```css +.stars .star:hover, +.stars .star:hover ~ .star { + opacity: 1; +} +``` + + + +## Финальный код + +Разметка: + +```html +
    + Оцените товар: +
      +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    +``` + +Стили: + +```css +button { + background-color: transparent; + border: none; + color: inherit; + cursor: pointer; + font: inherit; + padding: 0; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + clip: rect(0 0 0 0); + overflow: hidden; +} + +.star-rating { + text-align: center; + border: none; +} + +.stars { + list-style: none; + display: flex; + flex-direction: row-reverse; + gap: 1rem; + font-size: 5rem; +} + +.star { + opacity: 0.5; + cursor: pointer; + transition: opacity 0.2s; +} + +.stars:hover .star { + opacity: 0.5; +} + +.active, +.active ~ .star { + opacity: 1; +} + +.stars .star:hover, +.stars .star:hover ~ .star { + opacity: 1; +} +``` + +JavaScript: + +```javascript +const rating = document.querySelector('.star-rating') +const stars = document.querySelectorAll('.star') + +stars.forEach((star, index) => { + star.addEventListener('click', (event) => { + const activeStar = event.currentTarget + + if (activeStar.classList.contains('active')) { + activeStar.classList.remove('active') + rating.dataset.ratingValue = '' + } else { + stars.forEach(star => star.classList.remove('active')) + activeStar.classList.add('active') + rating.dataset.ratingValue = parseInt(activeStar.textContent.trim()) + } + }) +}) +``` + + From edb8bb6220f6ea30b3bf8b4a70a3b3ac39fc1598 Mon Sep 17 00:00:00 2001 From: Alena Batitskaia Date: Wed, 11 Dec 2024 20:03:56 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D1=8F?= =?UTF-8?q?=D0=B5=D1=82=20=D1=80=D0=B5=D1=86=D0=B5=D0=BF=D1=82=20=D0=B2=20?= =?UTF-8?q?=D0=B8=D0=BD=D0=B4=D0=B5=D0=BA=D1=81=20=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D0=BD=D1=8B=D0=B5=D1=82=20related?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- recipes/index.md | 1 + recipes/star-rating/index.md | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/recipes/index.md b/recipes/index.md index 08870438a1..03592d4ddd 100644 --- a/recipes/index.md +++ b/recipes/index.md @@ -17,6 +17,7 @@ groups: - masonry - slider - checkbox-radio-style + - star-rating - name: 'Работа с GitHub' items: - github-new-profile diff --git a/recipes/star-rating/index.md b/recipes/star-rating/index.md index aa40b5e388..ca60d43a6c 100644 --- a/recipes/star-rating/index.md +++ b/recipes/star-rating/index.md @@ -3,6 +3,10 @@ title: "Рейтинг «5 звёзд»" description: "Популярный элемент для оценки по пятибальной шкале на чистом HTML, CSS и JavaScript." authors: - solarrust +related: + - css/hover + - js/query-selector + - css/cascade/ tags: - article --- From a2a005831ca57895a6daca4a95a7537d70f978a3 Mon Sep 17 00:00:00 2001 From: Svetlana Korobtseva Date: Fri, 13 Dec 2024 18:56:44 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=D0=94=D0=B5=D0=BB=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=BF=D0=BE=D0=B1=D0=BE=D0=BB?= =?UTF-8?q?=D0=B1=D1=88=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- recipes/star-rating/demos/css-1/index.html | 7 ++++++- recipes/star-rating/demos/css-2/index.html | 7 ++++++- recipes/star-rating/demos/css-3/index.html | 7 ++++++- recipes/star-rating/demos/html/index.html | 3 --- recipes/star-rating/demos/js-1/index.html | 7 ++++++- recipes/star-rating/demos/js-2/index.html | 7 ++++++- recipes/star-rating/demos/js-3/index.html | 7 ++++++- recipes/star-rating/demos/js-4/index.html | 7 ++++++- recipes/star-rating/demos/js-css/index.html | 7 ++++++- recipes/star-rating/demos/result/index.html | 7 ++++++- recipes/star-rating/index.md | 10 ++++++++++ 11 files changed, 64 insertions(+), 12 deletions(-) diff --git a/recipes/star-rating/demos/css-1/index.html b/recipes/star-rating/demos/css-1/index.html index 6da2758e81..487b105741 100644 --- a/recipes/star-rating/demos/css-1/index.html +++ b/recipes/star-rating/demos/css-1/index.html @@ -6,7 +6,7 @@ - +