Wstęp
Jak możemy zauważyć – strony i aplikacje internetowe nie odstępują nas na krok. Są narzędziem np. do komunikacji, pracy, czy robienia zakupów, z których korzystają miliony ludzi na całym świecie. Niestety, nie wszystkie strony są tworzone z myślą o osobach niepełnosprawnych. Według raportu WebAIM (Web Accessibility In Mind) z 2023 roku, aż 96,3% stron internetowych nie spełnia podstawowych wymagań by takie osoby mogły w pełni korzystać z serwisu. Najczęstsze problemy związane z dostępnością obejmują:
- Brak tekstów alternatywnych dla obrazów (67,9% stron)
- Brak odpowiedniego kontrastu tekstu w stosunku do tła (86,3% stron)
- Brak etykiet lub niepoprawne etykiety dla pól formularzy (68% stron)
- Problemy z nawigacją klawiaturą (59,6% stron)
Jest więc to ważny aspekt, nad którym warto się pochylić, podczas tworzenia stron internetowych.
Co na to Angular?
Na pewno warto przyjrzeć się paczce a11y z Angular CDK. Zawiera ona najbardziej powszechne rozwiązania, które zwiększają współczynnik dostosowania strony do osób niepełnosprawnych.
Sprawdźmy ogółem, na co warto zwrócić uwagę.
Struktura HTML
Dobrze wiemy o tym, jak poprawne wykorzystanie tagów HTML przyczynia się do korzystniejszego pozycjonowania stron (SEO) czy do zgodności ze standardami sieciowymi. Niewielu jednak pamięta, jaką rolę odgrywają one w technologiach wspomagających czytanie strony np. osobom niewidomym lub korzystającym z klawiatury. Znaczniki semantyczne dostarczają dodatkowych informacji dla czytników ekranowych, co ułatwia zrozumienie struktury i zawartości strony. Co więcej upraszczają nawigację po stronie za pomocą klawiatury i innych narzędzi wspomagających, ponieważ elementy takie jak <nav> i <aside> są rozpoznawane jako sekcje nawigacyjne.
Warto więc dzielić komponenty na różne sekcje – unikać korzystania wyłącznie z tagów <div> czy <p>, a wzbogacać strukturę o wiele innych – takich chociażby jak
- <article>, <section> – do grupowania kontentu
- <nav> – do grupowania linków
- <details> – do wprowadzenia dodatkowych szczegółów, które użytkownik może otwierać i zamykać na żądanie
- <mark> – do podkreślania / podświetlania ważnego fragmentu tekstu
i wiele innych, dzięki którym znacznie poprawimy nawigowanie po aplikacji.
NIE | TAK |
|
|
Spójrzmy również na taki przykład:
<div role="button">Save</div>
i
<button>Save</button>
Oba rozwiązania odpowiadają za tę samą funkcję, jednak nie oba są w pełni dobrą praktyką. Spójrzmy na konsekwencje ich użycia:
- <button> jest domyślnie interpretowany przez przeglądarki jako przycisk z odpowiednim wyglądem i zachowaniem. <div> wymaga dodatkowego kodu by posiadać takie właściwości
- <button> ma domyślną rolę button. Dla <div> trzeba ręcznie dodać role=’button’ oraz odpowiednie atrybuty ARIA, aby czytniki ekranowe mogły poprawnie interpretować element jako przycisk.
- <button> można aktywować za pomocą klawiszy klawiatury. <div> wymaga atrybutów tabindex (o którym powiemy więcej w punkcie ‘Wykorzystanie klawiatury’) oraz obsługi zdarzeń klawiszowych
- <button> ma domyślnie właściwości przydatne w formularzach, takie jak type=’submit’, type=’reset’. <div> ich nie posiada przez co wymaga dodatkowego kodu
Czytnik ekranowy
Jest to program komputerowy, który rozpoznaje tekst na stronie (w tym ten ukryty) i konwertuje go na wiadomość głosową lub wysyła do brajlowskiego urządzenia. Jak z nim współpracować?
Możemy używać atrybutów HTML, w tym atrybutów ARIA (Accessible Rich Internet Applications) dostępnych w standardzie HTML.
Atrybuty to dodatkowe ustawienia znaczników, które możemy dodać do zastosowanych tagów HTML, jeżeli te nie są wystarczające, i za pomocą których można zmieniać właściwości i zachowanie elementu. Dzięki temu elementy strony są uzupełnione o niezbędne czytnikom informacje. Programiści mogą początkowo nie dostrzegać zalet ich użycia. Wydają się one być pozornie zbędnym opisem, jednak w rzeczywistości są kluczowe dla osób, które niestety nie mogą zobaczyć, ale mogą usłyszeć.
Pamiętajmy jednak o tym, by w miarę możliwości w pierwszej kolejności skupiać się na wykorzystaniu odpowiednich tagów HTML, a po dodatkowe atrybuty sięgać w przypadku, gdy same tagi nie wystarczą.
Oto kilka przykładów atrybutów:
- aria-label – nadaje nazwę elementowi, szczególnie gdy ten element nie posiada tekstu, np. ikona, zdjęcie
<button mat-icon-button aria-label='Share our blog'>share</button>
- aria-description – pozwala nałożyć na element dodatkowy opis
<button mat-icon-button aria-description="Click here for more info about the article">info️</button>
- aria-hidden – może służyć do ukrywania nieinteraktywnych elementów DOM
<mat-icon aria-hidden="true" class="only-aesthetic"></mat-icon>
- aria-live – informuje na bieżąco o zmianie zawartości elementu, w podanym przykładzie typ polite określa, że komunikat zostanie wypowiedziany przy najbliższej możliwej okazji, np. po zakończeniu wypowiadania bieżącego zdania
<div>
<button [disabled]="isDisabled">Save</button>
<div class="alert" aria-live="polite">{{ isDisabled ? 'Button is active' : 'Button is inactive' }}</div>
</div>
- aria-orientation – określa orientację elementu poziomą lub pionową, np. pasek nawigacji czy menu
<ul aria-orientation="vertical" class="menu">
<li></li>
<li></li>
</ul>
- alt – alternatywny tekst do obrazka
<img src="img_girl.jpg" alt="Angular Love logo"/>
Możemy tutaj zauważyć, że nie musimy wypełniać atrybutu np. taką frazą ‘Obrazek przedstawiający logo bloga Angular Love’. Czytniki ekranowe same przechwytują te informacje na podstawie zastosowanych tagów i uwzględniają je podczas konwertowania treści.
Z pomocą przychodzi także LiveAnnouncer, który – podobnie jak atrybut aria-live – pozwala wysłać do czytników dynamicznie pojawiające się komunikaty tekstowe, ale nie z poziomu HTML, a np. w funkcji, efekcie, serwisie.
private _liveAnnouncer = inject(LiveAnnouncer);
this._liveAnnouncer.announce('25 products found for your search query' );
Dostosowany UI/UX
Jedną z zasad opisanych w WCAG (Web Content Accessibility Guidelines) jest ta mówiąca o zachowaniu kontrastu kolorów na stronie. Minimalne proporcje powinny wynosić 4,5:1 poza kilkoma wyjątkami, gdzie stosunek może wynosić 3:1. Dlatego podstawowa kolorystyka każdej strony powinna być odpowiednio zaprojektowana zgodnie z tymi wymogami. Jednak co w przypadku, gdy nie jesteśmy w stanie dostarczyć tego kontrastu i powinniśmy zaimplementować wersję kontrastową? Lub jeżeli chcemy wprowadzic light mode i dark mode? Wystarczy, że posłużymy się zmiennymi css’owymi tworząc indywidualne palety kolorów, które będziemy ładować w zależności od wybranego ustawienia.
:root {
/* Light mode colors */
--primary-color-light: #3498db;
--background-color-light: #ffffff;
--text-color-light: #000000;
/* Dark mode colors */
--primary-color-dark: #2980b9;
--background-color-dark: #2c3e50;
--text-color-dark: #ecf0f1;
}
body.light-mode {
--primary-color: var(--primary-color-light);
--background-color: var(--background-color-light);
--text-color: var(--text-color-light); }
body.dark-mode { --primary-color: var(--primary-color-dark);
--background-color: var(--background-color-dark);
--text-color: var(--text-color-dark);
}
.expampleClass {
color: var (
--primary - color
)
}
Warto również pochylić się nad możliwością dostosowania naszej strony pod względem animacji. Redukcja tych animacji oraz ruchu animowanych elementów może wyraźnie poprawić dostępność cyfrową. Możemy je ustawić wedle preferencji użytkownika przy pomocy funcji CSS prefers-reduced-motion
/* Standardowe animacje */
.element {
transition: transform 0.5s ease-in-out;
}
/* Redukcja ruchu, gdy użytkownik preferuje mniej animacji */
@media (prefers-reduced-motion: reduce) {
.element {
transition: none;
}
}
Wykorzystanie klawiatury
Nie zawsze korzystanie z aplikacji może odbyć się za pomocą myszki. Dlatego tworząc ją powinniśmy zadbać o alternatywną opcję w postaci wykorzystania chociażby klawiszy klawiatury.
W sieci możemy znaleźć wiele rozwiązań i kilka z nich dostarcza również Angular. Możemy użyć na przykład biblioteki ngx-mousetrap, angular2-hotkeys lub chyba najbardziej popularnej dyrektywy @HostListener. Wszystkie te narzędzia pozwolą nam przechwycić wywoływane klawisze, czy też skróty klawiszowe, i dostosować do nich działania, np. otworzenie pola z wyszukiwaniem elementów.
- angular2-hotkeys
private _hotkeysService = inject(HotkeysService);
ngOnInit() {
this._hotkeysService.add(new Hotkey('ctrl+s', (event: KeyboardEvent): boolean => {
event.preventDefault();
}
));
}
- ngx-mousetrap
private _mousetrap= inject(NgxMousetrapService);
ngOnInit() {
this.mousetrap.bind('ctrl+s', (e: KeyboardEvent) => {
e.preventDefault(); this.saveDocument();
}
);
}
- @HostListener
@HostListener('keydown.enter', ['$event']) onEnterKeyDown(
e: KeyboardEvent,
): void {
event.preventDefault();
}
Biblioteka a11y udostępnia także gotowe narzędzia do zarządzania listą elementów, np.
- ListKeyManager – dzięki której możemy w łatwy sposób zarządzać elementami listy, np.: nawigowanie po elementach menu używając strzałek, powrót z ostatniego elementu na pierwszy (funkcja withWrap()), przeskok do konkretnego elementu.
items = ['Item 1', 'Item 2', 'Item 3'];
items = viewChildren<QueryList<MatListItem>>('itemRef', { read: MatListItem })
keyManager: ListKeyManager<MatListItem>;
ngAfterViewInit() {
this.keyManager = new ListKeyManager(this.items).withWrap();
}
- ActiveDescendantKeyManager – zaawansowana wersja ListKeyManager, która dodatkowo śledzi i zarządza aktywnym elementem
items = ['Item 1', 'Item 2', 'Item 3'];
itemElements = viewChildren<QueryList<ElementRef>>('itemRef')
keyManager: ActiveDescendantKeyManager<ElementRef>;
ngAfterViewInit() {
this.keyManager = new ActiveDescendantKeyManager(this.itemElements).withWrap();
}
// funkcja isActive() sprawdza czy dany element jest aktywny porównując jego zawartość tekstową z już aktywnym elementem
isActive(item: string): boolean {
return this.keyManager.activeItem && this.keyManager.activeItem.nativeElement.textContent.trim() === item;
}
Na początku artykułu wspomnieliśmy o atrybucie HTML tabindex. Jest on używany do zarządzania kolejnością nawigacji przy pomocy klawiatury (tabulacji) oraz do definiowania elementów nieinteraktywnych. Przejrzyjmy kilka przykładów kiedy możemy go użyć:
- możemy nadpisać niestandardowo domyślną kolejność elementów, w tym celu ustawiamy na atrybucie rosnąco wartości dodatnie
<input type="text" tabindex="2">
<input type="text" tabindex="1">
<input type="text" tabindex="3">
- tabindex=’0’ sprawi, że elementy nieinteraktywne (np. <div>, <span>, <p>) staną się częścią sekwencji tabulacji i staną sie focusowalne po elementach mających tabindex większe od 0
<div tabindex="0">You can click me</div>
- tabindex=’-1’ usuwa element z kolejności tabulacji, jest to przydatne gdy element powinien być interaktywny, ale nie powinien być dostępny przez zwykłe tabulowanie
<a tabindex="-1">Hidden link</a>
Poprawne użycie tabindex pomaga w tworzeniu logicznej kolejności nawigacji, szczególnie gdy ta kolejność jest inna niż może sugerować układ danych elementów. Warto jednak mieć na uwadze, że nie jest to zalecane rozwiązanie, ponieważ może wprowadzić użytkownika w zakłopotanie. Na przykład wszystkie elementy o wartości tabindex=’1’ zostaną sfocusowane przed tymi o wartości tabindex=’2’ i tabindex=’0’, dlatego w praktyce zaleca się używanie jedynie wartości 0 i -1.
Pamiętajmy więc o tym, że podobnie jak dobrą praktyką jest używanie dostosowanych tagów HTML, tak tabindexu powinniśmy używać tylko w niestandardowych sytuacjach, gdzie dostępne narzędzia nie rozwiążą wystarczająco naszego problemu.
W parze z obsługą klawiatury idzie również aspekt podświetlania bądź zaznaczania aktywnego elementu. Domyślny styl outline w przeglądarkach zapewnia widoczny wskaźnik dla elementów w stanie focus. Osoby korzystające z technologii wspomagających, takich jak czytniki ekranu, również polegają na tym wskaźniku wizualnym, aby zorientować się, na jakim elemencie się aktualnie znajdują.
Nie zawsze jednak domyślny outline przeglądarki pasuje do designu strony. Jest to przeważnie czarna lub niebieska obwódka wokół elementu i tym samym jest to jedna z najczęstszych przyczyn decyzji o usunięciu tej funkcji. Mimo to nie powinniśmy tego robić. Zamiast tego możemy zaimplementować własne style, których efekt będzie spójny z walorami naszej strony.
Autofocus
Autofocus na stronach internetowych automatycznie zaznacza konkretny element interaktywny, np po załadowaniu strony, czy po otwarciu dialogu. Chociaż może to być użyteczne w wielu przypadkach, tak dla osób niepełnosprawnych przy nie obsłużonym autofocusie może się to okazać prawdziwą zmorą. Dlaczego?
- Użytkownicy, którzy polegają na nawigacji klawiaturą, mogą zostać zdezorientowani, gdy fokus nagle przeskoczy na określone pole.
- Jeśli autofocus przeniesie fokus na inne pole, czytnik ekranu może natychmiast przejść do czytania zawartości tego pola, pomijając inne ważne informacje na stronie.
- Autofocus może powodować niespodziewane zmiany kontekstu. Użytkownicy mogą być zmuszeni do przeskakiwania w różne miejsca na stronie bez jasnego powodu, co może być frustrujące i dezorientujące.
Nie skupiajmy się jednak na samych negatywach autofocusu.
Wiele gotowych komponentów, np. Dialog z Angular Material domyślnie ustawia focus na action-buttonach lub innych elementach, co może stwarzać pewne problemy w nawigacji. Jednakże możemy spotkać przypadek, gdy otwieramy modal z search-barem za pomocą buttona czy skrótu klawiszowego i defacto spodziewamy się, że focus ustawi się domyślnie na polu tekstowym. Dlatego w przypadku dostosowywania strony do osób niepełnosprawnych starajmy się pamiętać, aby tym autofocusem odpowiednio zarządzać. Poniżej możemy zobaczyć przykłady:
- manualne wyłączenie autofocusu na otwarciu dialogu
this.dialog.open(YourDialogComponent, {
autoFocus: false
});
- wykorzystanie FocusKeyManager – jest to rozszerzenie ListKeyManager, które dodatkowo automatycznie ustawia focus na elementy listy
items = viewChildren<QueryList<MatListItem>>(‘itemRef’);
private keyManager: FocusKeyManager<MatListItem>;
itemsArray = ['Item 1', 'Item 2', 'Item 3', 'Item 4'];
ngAfterViewInit() {
this.keyManager = new FocusKeyManager(this.items).withWrap();
}
- wykorzystanie dyrektywy CdkTrapFocus – pozwala ograniczyć możliwość interakcji na stronie do jakiegoś fragmentu, nałożenie jej (np. tymczasowo) na dany element nie pozwala użytkownikowi zmienić focusu na inny element, np. klikając klawisz Tab
<div *ngIf="dialogOpen" class="dialog" cdkTrapFocus>
Testowanie dostępności
Starajmy się regularnie sprawdzać czy nasza aplikacja zawsze spełnia standardy dostępności cyfrowej. Istnieją do tego gotowe narzędzia, takie jak:
- Lighthouse (wbudowane w Chrome DevTools)
- Axe (wtyczka do przeglądarek)
- Screen readers (np. NVDA, JAWS)
Podsumowanie
Przystosowanie aplikacji internetowej do osób niepełnosprawnych to szerokie zagadnienie. Angular dostarcza nam wiele rozwiązań i technik w tym obszarze, dzięki czemu nie musimy ich szukać w zewnętrznych bibliotekach. Wprowadzenie m.in. powyższych praktyk i narzędzi w procesie tworzenia aplikacji nie tylko poprawia dostępność cyfrową dla osób niepełnosprawnych, ale także korzystnie wpływa na ogólne doświadczenie użytkowników, czyniąc strony bardziej przyjazne i łatwiejsze w użyciu dla wszystkich.