15 paź 2025
6 min

Case Study: Tworzenie dostępnego selektora wariantów

Dostępność webowa, chociaż często pomijana, niesie ze sobą wiele korzyści. Ten artykuł bierze pod lupę standardowy selektor wariantów oparty na kafelkach, początkowo stworzony po linii najmniejszego oporu, omawiając jego wady i przedstawiając jak można usprawnić jego dostępność dla wszystkich użytkowników.

Kontekst

Tworzenie dostępnych interfejsów opiera się na właściwym użyciu semantycznych elementów HTML. Może się to wydawać drobnym szczegółem, zwłaszcza że event binding w Angularze działa z dowolnym tagiem HTML, a CSS może zostać wykorzystany do osiągnięcia pożądanego efektu wizualnego. Jednak znaczenie semantycznego HTML wykracza poza te rozważania.

<div class="primary-button" (click)="onClick()">Click me!</div>

Klikalny element zbudowany w ten sposób niesie ze sobą kilka problemów:

  • Negatywny wpływ na developer experience: Pogarsza czytelność kodu, utrudniając szybką identyfikację przeznaczenia elementu.
  • Szkodliwe dla SEO: Pozycja Twojej witryny w wynikach wyszukiwarek spada.
  • Kłopotliwa nawigacja: Użytkownicy korzystający z technologii wspomagających mają trudności z nawigacją na stronie.
  • Brak wbudowanej interaktywności: W przeciwieństwie do części elementów HTML, <div> nie jest domyślnie interaktywny.

Chociaż technicznie możliwe jest wymuszenie na elemencie <div> zachowania zgodnego z zamierzeniem, powstały kod jest często zagmatwany i nieefektywny:

<div
 class="primary-button"
 role="button"
 tabindex="0"
 style="cursor: pointer"
 (click)="onClick()"
 (keydown.space)="onClick(); $event.preventDefault()"
 (keydown.enter)="onClick()"
>
 Click me!
</div>

Aby odtworzyć zachowanie standardowego elementu <button> za pomocą diva, konieczne są pewne modyfikacje:

  • Nadanie role="button", aby technologie wspomagające rozpoznały go jako przycisk.
  • Ustawienie atrybutu tabindex na 0, co umożliwia nawigację do elementu klawiaturą za pomocą klawisza TAB.
  • Zastosowanie stylu cursor: pointer, który jest standardem dla przycisków.
  • Oprócz obsługi (click) eventu, należy obsłużyć także (keydown) event dla klawiszy Spacji i Enter, odzwierciedlając natywną funkcjonalność przycisku.
  • Podczas obsługi Spacji wywołanie $event.preventDefault(), aby zapobiec domyślnemu przewijaniu strony.

Takie akrobacje najczęściej nie są optymalnym rozwiązaniem, zwłaszcza biorąc pod uwagę prostotę osiągniętą przy zastosowaniu standardowego podejścia:

<button class="primary-button" (click)="onClick()">Click me!</button>

Dlaczego powinienem zwracać uwagę na dostępność?

Na tym etapie prawdopodobnie zadałeś już sobie to pytanie – i słusznie. Choć dla większości użytkowników poruszanie się po sieci to kwestia kilku kliknięć, nie wszyscy mają taką swobodę.

Powody mogą być różne – od trudności z używaniem myszy spowodowanych drżącymi rękami, po całkowite poleganie na asystentach głosowych, które „pod spodem” wywołują polecenia klawiaturowe.

Aby zrozumieć, jak poruszać się po stronach internetowych wyłącznie za pomocą klawiatury, spójrz na poniższy kod i poeksperymentuj z wyrenderowanymi elementami:

<fieldset>
  <legend>Wybierz swój ulubiony owoc</legend>
  <div>
    <label for="apple">Jabłko</label>
    <input type="radio" id="apple" name="fruit" value="apple" />
  </div>
 <!-- Pozostałe elementy... -->
</fieldset>
<br />
<fieldset>
  <legend>Wybierz swoje ulubione warzywo</legend>
  <div>
    <label for="carrot">Marchew</label>
    <input type="radio" id="carrot" name="vegetable" value="carrot" />
  </div>
   <!-- Pozostałe elementy... -->
</fieldset>
Wybierz swój ulubiony owoc
Wybierz swoje ulubione warzywo
 
  1. Użyj przycisku Tab, aby przełączać fokus do kolejnych elementów interaktywnych, aż dotrzesz do pierwszego radio buttona w grupie „Wybierz swój ulubiony owoc”.
  2. Użyj klawiszy strzałek (Góra/Dół lub Lewo/Prawo), aby poruszać się między opcjami w tej samej grupie.
  3. Naciśnij Tab ponownie, aby przejść do następnego interaktywnego elementu (tj. do grupy „Wybierz swoje ulubione warzywo”).
  4. Użyj Shift + Tab, aby powrócić do grupy „Wybierz swój ulubiony owoc”.
  5. Pamiętaj, że tylko jeden radio button na grupę może być zaznaczony w danym momencie.

Te interakcje są obsługiwane przez wszystkie nowoczesne przeglądarki, zapewniając dostępność dla użytkowników klawiatury. Ważne jest, aby tworzyć komponenty dostępne dla każdego, nie tylko tych, którzy polegają na myszy.

Przedstawienie problemu


Teraz skupmy się na sednie tego artykułu: budowaniu selektora wariantów T-shirtów opartego na kafelkach. Niezbędnym założeniem jest umożliwienie nawigacji także za pomocą klawiatury. Powyższy przykład jasno to demonstruje – kursor znajdujący się obok kafelków wskazuje, że nawigacja między elementami jest możliwa bez użycia myszy. Osiągnięcie tego wymaga odpowiedniego kodu HTML.

Niewłaściwe podejście

Wielu deweloperów, gdy spotyka się z elementem interfejsu takim jak kolekcja obrazów ułożonych w kafelki, instynktownie wyobraża go sobie jako standardową serię elementów <img>. W efekcie tworzą szablon, który wyglądałby mniej więcej tak:

<div class="variant-selector__selected-color">
 Selected color: @if (activeVariant()?.name) {
 <strong>{{ activeVariant()!.name }}</strong>
 }
</div>
<div class="variant-selector__tiles-container">
 @for (variant of variants(); track variant.id) {
 <img
   class="variant-selector__image"
   [class.variant-selector__image--active]="variant.id === activeVariant()?.id"
   [ngSrc]="variant.thumbnailSrc"
   alt="{{ variant.name }} T-Shirt"
   width="74"
   height="80"
   (click)="variantSelected.emit(variant)"
 />
 }
</div>

Chociaż pozornie akceptowalne, takie podejście nie zapewnia działania z klawiaturą. Dzieje się tak, ponieważ elementy <img> nie są domyślnie interaktywne, co uniemożliwia nawigowanie do nich za pomocą klawiatury.

Rozwiązanie

Patrząc na to z innej perspektywy, ten przykład dokładnie odzwierciedla standardową grupę radio buttonów w HTML: to kolekcja kontrolek, z których tylko jedna może być aktywna w danym momencie. Mając to na uwadze, dopracujmy nasz początkowy kod:

<fieldset role="radiogroup">
 <legend class="variant-selector__selected-color">
   Selected color: @if (activeVariant()?.name) {
   <strong>{{ activeVariant()!.name }}</strong>
   }
 </legend>
 <div class="variant-selector__tiles-container">
   @for (variant of variants(); track variant.id) {
   <label>
     <input
       [attr.aria-label]="variant?.name"
       class="cdk-visually-hidden"
       type="radio"
       name="variant"
       [value]="variant.id"
       [checked]="variant.id === activeVariant()?.id"
       (change)="variantSelected.emit(variant)"
     />
     <img
       class="variant-selector__image"
       [ngSrc]="variant.thumbnailSrc"
       alt="{{ variant.name }} T-Shirt"
       width="74"
       height="74"
     />
   </label>
   }
 </div>
</fieldset>

Wprowadzone zmiany

Zmodyfikowaliśmy nasze podejście, zastępując elementy <img> niewidocznymi wizualnie elementami <input type="radio">. Osiągnęliśmy to dzięki użyciu klasy cdk-visually-hidden dostarczonej przez Angular CDK, która ukrywa okrągłe pola radio buttonów, zachowując jednocześnie możliwość ustawienia na nich fokusu.

Możesz się zastanawiać: dlaczego nie użyć po prostu visibility: hidden albo display: none? Różnica polega na tym, że klasa cdk-visually-hidden została stworzona specjalnie po to, aby ukrywać elementy wizualnie, ale jednocześnie zachować ich interaktywność i dostępność dla technologii asystujących. Dzięki temu użytkownicy mogą teraz poruszać się po wariantach i wybierać je za pomocą klawiatury, dokładnie tak, jak w standardowych grupach radio buttonów omawianych wcześniej. Jeśli w projekcie korzystasz z Tailwind CSS, możesz zamiast tego użyć klasy sr-only, która działa w identyczny sposób.


Dla lepszego pogrupowania semantycznego i czytelnego etykietowania zastosowaliśmy elementy <fieldset> oraz <legend>. Aby dodatkowo wesprzeć użytkowników czytników ekranu, dodaliśmy atrybut aria-label do naszych inputów. Podczas gdy osoby widzące mogą łatwo rozpoznać kafelki wizualnie (np. konkretny kolor koszulki), użytkownicy korzystający z technologii asystujących polegają na komunikatach głosowych generowanych przez czytniki ekranu. Dzięki aria-label komunikaty te stają się zrozumiałe i opisowe – przykładowo mogą brzmieć „Yellow T-Shirt” lub „Żółty podkoszulek„.

Podsumowanie

Dostępność to nie tylko abstrakcyjne pojęcie dotyczące wąskiej grupy użytkowników – przynosi korzyści absolutnie wszystkim.

  • Osoby z niepełnosprawnościami zyskują pełny dostęp do produktu.
  • Deweloperzy czerpią z lepszego developer experience, pracując z bardziej przyjaznym kodem, głównie dzięki oparciu go o natywne zachowania przeglądarki.
  • Firma natomiast ogranicza ryzyko finansowe, poprawia SEO i jednocześnie utrzymuje wysokie standardy etyczne.

Dla osób, które chciałyby bliżej poznać rozwiązanie opisane w tym artykule, dostępne jest repozytorium z kodem. Można tam prześledzić historię commitów – zaczynając od początkowego „złego” podejścia, a następnie krok po kroku obserwować zmiany opisane w artykule.

Jeśli natomiast wolisz treści wideo, polecam moje wystąpienie na Angular Camp, wydarzeniu organizowanym przez angular.love. W prezentacji nie tylko na żywo pokazuję, jak przejść od mniej dostępnego rozwiązania do poprawionego, ale też przedstawiam solidne wprowadzenie do tematyki dostępności.

Podziel się artykułem

Zapisz się na nasz newsletter

Dołącz do community Angular.love i bądź na bieżąco z trendami.