Wpadki i wypadki #12

Zapraszam na kolejne Wpadki i wypadki!

Artykuł został zaktualizowany 29 sierpnia 2021, by dodać najnowsze dobre praktyki odnośnie przewijania.

Dzisiaj na tapet wzięty zostanie wzorzec, który swego czasu już omawiałem: nawigacja na stronie typu one-page, płynnie przewijająca do odpowiednich sekcji na stronie. Zacznijmy od prostego przykładu:

<nav class="navigation">
	<ul class="navigation__menu menu">
		<li class="menu__item">
			<a href="#" id="about-me-link" class="menu__link">About me</a>
		</li>
		<li class="menu__item">
			<a href="#" id="offer-link" class="menu__link">Offer</a>
		</li>
		<li class="menu__item">
			<a href="#" id="contact-link" class="menu__link">Contact</a>
		</li>
	</ul>
</nav>
[…]
<script src="http://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
<script>
	$( '.menu__link' ).on( 'click', ( evt ) => {
		const targetId = evt.target.id.replace( /-link$/, '' );
		const target = $( '#' + targetId );

		$( 'html, body' ).animate({
			scrollTop: target.offset().top
		} );
	} );
</script>

Tego typu rozwiązanie spotykam na sporej liczbie stron, które wrzucane są do oceny na grupach na FB czy forach. Choć działa, jest z nim szereg problemów:

  • URL-e są częścią UI. Psując kotwice do poszczególnych sekcji strony, psujemy tak podstawowe rzeczy, jak m.in. możliwość podzielenia się linkiem do konkretnej sekcji naszej strony, np. kontaktu.

  • Brak poprawnego URL-a obniża znacząco także i użyteczność strony, nie pozwalając choćby otworzyć danej sekcji w nowej karcie. Bardzo często klikam linki w nawigacji kółkiem myszy i bardzo nie lubię, gdy okazuje się, że link w nawigacji przeniósł mnie nigdzie.

  • Co więcej, uzależniamy działanie naszej nawigacji od działania JS-a – a ten lubi się wysypać. Ba, nawigacja zacznie działać dopiero, gdy JS się wczyta, więc skonsternowany użytkownik może przez kilka sekund klikać w niedziałające odnośniki. A jeśli jest bardziej paranoiczny i ma włączonego choćby NoScripta, to linki nigdy nie zaczną działać.

  • Bez zmiany URL-a nie zmienia się także focus na stronie. A to oznacza, że zostaje w menu. Można to łatwo przetestować na przykładzie, po prostu naciskając Tab po przejściu do dowolnej sekcji. Zamiast przenieść focus na przycisk wewnątrz danej sekcji, zostaniemy przerzuceni z powrotem na górę strony, do nawigacji. Istnieją co prawda obejścia tego problemu, ale nie rozwiązują one z kolei pozostałych wymienionych problemów, zwłaszcza braku zmiany URL-a.

  • Tego typu animacja przewijania nie zważa na to, czy użytkownik nie włączył przypadkiem ograniczenia ruchu w swoim systemie.

Jak zatem zrobić taką nawigację poprawnie? Oto przykład:

<head>
	[…]
	<style>
		@keyframes smoothscroll1 {
			from, to { scroll-behavior: smooth; }
		}

		@keyframes smoothscroll2 {
			from, to { scroll-behavior: smooth; }
		}

		:root {
			animation: smoothscroll1 1s;
		}

		:root:focus-within {
			animation-name: smoothscroll2;
			scroll-behavior: smooth;
		}

		@media ( prefers-reduced-motion ) {
			:root, :root:focus-within {
				animation: none;
				scroll-behavior: auto;
			}
		}

		.section:focus {
			outline: none;
		}

		.section:target {
			background-color: red;
		}
	</style>
</head>
<body>
	[…]
	<nav class="navigation">
		<ul class="navigation__menu menu">
			<li class="menu__item">
				<a href="#about-me" class="menu__link">About me</a>
			</li>
			<li class="menu__item">
				<a href="#offer" class="menu__link">Offer</a>
			</li>
			<li class="menu__item">
				<a href="#contact" class="menu__link">Contact</a>
			</li>
		</ul>
	</nav>

	<section id="about-me" class="section" tabindex="-1">
		<h2 class="section__title">About me</h2>

		<p><button>Focus me</button></p>
		<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Animi distinctio quam, ipsum eius qui vel doloribus earum iusto eos deserunt alias aspernatur voluptatem soluta? Soluta culpa doloremque repellat possimus inventore.</p>
		<p>Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quod cumque maxime aliquid nihil sequi molestiae quaerat quidem dicta. Natus aliquam perspiciatis eaque? Quia modi soluta libero perferendis corporis, quaerat voluptate.</p>
		<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Unde culpa, eum tempora officia voluptates hic in asperiores sapiente veritatis eius officiis praesentium aliquid sint possimus, sit iste itaque repudiandae eos?</p>
	</section>

	[…]

	<script>
		{
			const isReducedMotion = window.matchMedia( '( prefers-reduced-motion )' ).matches;

			if ( !( 'scrollBehavior' in document.documentElement.style ) && !isReducedMotion ) {
				import( 'https://unpkg.com/smoothscroll-polyfill@0.4.4/dist/smoothscroll.js' ).then( () => {
					document.querySelector( '.navigation' ).addEventListener( 'click', ( evt ) => {
						const link = evt.target.closest( '.menu__link' );

						if ( !link ) {
							return;
						}

						evt.preventDefault();

						const scrollToElement = link.getAttribute( 'href' );

						document.querySelector( scrollToElement ).scrollIntoView( {
							behavior: 'smooth' }
						);

						location.href = scrollToElement;
					} );
				} );
			}
		}
	</script>
</body>
  • Wszystkie linki w nawigacji mają poprawne URL-e, dzięki czemu można je udostępnić czy otworzyć w nowej karcie.

  • Dzięki poprawnym URL-om odpada także problem uzależnienia działania nawigacji od JS-a. Jeśli JS z jakiegoś powodu się nie doczyta, linki w nawigacji będą dalej działać. I to działać od samego początku, bez konieczności czekania na doczytanie jakiegokolwiek skryptu.

  • Choć samo dodanie hashu w adresie powinno przenosić focus, wymuszamy to poprzez dodanie atrybutu [tabindex=-1] do każdej sekcji. To chyba jedyny przypadek, w którym można usunąć style dla :focus (bo aktualnie podlinkowaną sekcję i tak pewnie chcemy jakoś ostylować).

  • Dodanie animowanego przewijania tylko wtedy, gdy focus jest na którymś elemencie strony (:focus-within), naprawia problem z płynnym przewijaniem w czasie szukania treści na stronie.

  • Wykorzystanie media query prefers-reduced-motion pozwala na wyłączenie animacji przewijania u użytkowników, którzy nie życzą sobie animacji.

  • Bonus: podlinkowanym sekcjom można nadać osobne style dzięki zastosowaniu pseudoklasy :target.

Co więcej, poprawnie zrobiony link tego typu nie potrzebuje ani linijki JS-a. Wszystko dzięki (nie tak już znowu) nowej własności CSS: scroll-behavior: smooth;. Wsparcie dla niej jest bardzo dobre. A jeśli już koniecznie musimy też zapewnić wsparcie dla Safari, można dołączyć dla niego (i tylko niego) polyfilla – co robi skrypt z przykładu. W przypadku IE myślę, że można sobie darować płynne przewijanie.

I to by było na tyle!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.