Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Accessibility (a11y) w React

Na stacji kosmicznej każdy członek załogi musi mieć dostęp do systemów sterowania - niezależnie od tego, czy obsługuje je wzrokiem, dotykiem czy komendami głosowymi. Dokładnie tak samo działa dostępność (accessibility, w skrócie a11y) w aplikacjach webowych. Budując interfejs w React, musimy zadbać o to, by każdy użytkownik mógł z niego korzystać - osoby niewidome używające czytników ekranu, osoby poruszające się tylko klawiaturą, czy osoby z zaburzeniami widzenia kolorów.

Dlaczego dostępność jest ważna?

Według WHO, ponad miliard ludzi na świecie żyje z jakąś formą niepełnosprawności. To oznacza, że aplikacja niedostępna wyklucza ogromną grupę potencjalnych użytkowników. Poza aspektem etycznym, dostępność:

  • Jest wymagana prawnie w wielu krajach (WCAG, ADA)
  • Poprawia SEO i ogólną jakość kodu
  • Ułatwia korzystanie z aplikacji WSZYSTKIM użytkownikom (np. na urządzeniach mobilnych)
  • Sprawia, że interfejs jest bardziej przewidywalny i testowalny

Semantyczny HTML w React

Fundamentem dostępności jest używanie właściwych elementów HTML. React zachęca do tworzenia komponentów, ale łatwo wpaść w pułapkę nadużywania

<div>
:

1// ZLE - "div soup" - czytnik ekranu nie rozumie struktury
2function BadNavigation() {
3  return (
4    <div className="nav">
5      <div className="nav-item" onClick={goHome}>Start</div>
6      <div className="nav-item" onClick={goMissions}>Misje</div>
7      <div className="nav-item" onClick={goCrewPanel}>Załoga</div>
8    </div>
9  );
10}
11
12// DOBRZE - semantyczne elementy HTML
13function GoodNavigation() {
14  return (
15    <nav aria-label="Menu główne stacji">
16      <ul>
17        <li><a href="/">Start</a></li>
18        <li><a href="/missions">Misje</a></li>
19        <li><a href="/crew">Załoga</a></li>
20      </ul>
21    </nav>
22  );
23}

Kluczowe semantyczne elementy to:

<nav>
,
<main>
,
<header>
,
<footer>
,
<section>
,
<article>
,
<aside>
,
<button>
,
<form>
,
<label>
.

Atrybuty ARIA w React

ARIA (Accessible Rich Internet Applications) to zbiór atrybutów, które dodają informacje o roli i stanie elementów dla technologii asystujących. W React używamy ich w formacie camelCase lub z myślnikami:

1function MissionAlert({ message, severity }) {
2  return (
3    <div
4      role="alert"
5      aria-live="assertive"
6      aria-atomic="true"
7      className={severity}
8    >
9      {message}
10    </div>
11  );
12}
13
14function SearchField() {
15  const [results, setResults] = useState([]);
16  const [isOpen, setIsOpen] = useState(false);
17
18  return (
19    <div>
20      <label htmlFor="galaxy-search">Szukaj galaktyk:</label>
21      <input
22        id="galaxy-search"
23        type="search"
24        role="combobox"
25        aria-expanded={isOpen}
26        aria-controls="search-results"
27        aria-autocomplete="list"
28        aria-haspopup="listbox"
29      />
30      {isOpen && (
31        <ul id="search-results" role="listbox">
32          {results.map(r => (
33            <li key={r.id} role="option">{r.name}</li>
34          ))}
35        </ul>
36      )}
37    </div>
38  );
39}

Najczęściej używane atrybuty ARIA w React:

| Atrybut | Zastosowanie | |---------|-------------| |

role
| Definiuje rolę elementu (alert, dialog, tabpanel) | |
aria-label
| Tekstowa etykieta dla elementu | |
aria-labelledby
| ID elementu, który jest etykietą | |
aria-describedby
| ID elementu z dodatkowym opisem | |
aria-live
| Ogłasza dynamiczne zmiany treści | |
aria-expanded
| Czy element jest rozwinięty | |
aria-hidden
| Ukrywa element przed czytnikiem | |
aria-disabled
| Wskazuje element nieaktywny |

Zarządzanie fokusem

W aplikacjach SPA (Single Page Application) zarządzanie fokusem klawiatury jest krytyczne. Gdy użytkownik nawiguje klawiaturą, fokus musi przesuwać się w logicznej kolejności:

1import { useRef, useEffect } from 'react';
2
3function MissionModal({ isOpen, onClose, title, children }) {
4  const modalRef = useRef(null);
5  const closeButtonRef = useRef(null);
6
7  // Przenieś fokus do modala po otwarciu
8  useEffect(() => {
9    if (isOpen && closeButtonRef.current) {
10      closeButtonRef.current.focus();
11    }
12  }, [isOpen]);
13
14  // Pułapka fokusa - Tab nie wychodzi poza modal
15  const handleKeyDown = (e) => {
16    if (e.key === 'Escape') {
17      onClose();
18    }
19
20    if (e.key === 'Tab') {
21      const focusableElements = modalRef.current.querySelectorAll(
22        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
23      );
24      const firstElement = focusableElements[0];
25      const lastElement = focusableElements[focusableElements.length - 1];
26
27      if (e.shiftKey && document.activeElement === firstElement) {
28        e.preventDefault();
29        lastElement.focus();
30      } else if (!e.shiftKey && document.activeElement === lastElement) {
31        e.preventDefault();
32        firstElement.focus();
33      }
34    }
35  };
36
37  if (!isOpen) return null;
38
39  return (
40    <div
41      className="modal-overlay"
42      role="dialog"
43      aria-modal="true"
44      aria-labelledby="modal-title"
45      ref={modalRef}
46      onKeyDown={handleKeyDown}
47    >
48      <div className="modal-content">
49        <h2 id="modal-title">{title}</h2>
50        {children}
51        <button ref={closeButtonRef} onClick={onClose}>
52          Zamknij
53        </button>
54      </div>
55    </div>
56  );
57}

Nawigacja klawiaturą

Każdy interaktywny element musi być dostępny z klawiatury. React wspiera obsługę klawiszy przez

onKeyDown
,
onKeyUp
i
onKeyPress
:

1function SpaceshipSelector({ ships, onSelect }) {
2  const [activeIndex, setActiveIndex] = useState(0);
3
4  const handleKeyDown = (e) => {
5    switch (e.key) {
6      case 'ArrowDown':
7        e.preventDefault();
8        setActiveIndex(prev => Math.min(prev + 1, ships.length - 1));
9        break;
10      case 'ArrowUp':
11        e.preventDefault();
12        setActiveIndex(prev => Math.max(prev - 1, 0));
13        break;
14      case 'Enter':
15      case ' ':
16        e.preventDefault();
17        onSelect(ships[activeIndex]);
18        break;
19    }
20  };
21
22  return (
23    <ul
24      role="listbox"
25      aria-label="Wybierz statek kosmiczny"
26      tabIndex={0}
27      onKeyDown={handleKeyDown}
28    >
29      {ships.map((ship, index) => (
30        <li
31          key={ship.id}
32          role="option"
33          aria-selected={index === activeIndex}
34          onClick={() => onSelect(ship)}
35          style={{
36            background: index === activeIndex ? '#1a3a5c' : 'transparent',
37          }}
38        >
39          {ship.name} - {ship.type}
40        </li>
41      ))}
42    </ul>
43  );
44}

Ogłaszanie zmian dynamicznych (Live Regions)

Gdy treść strony zmienia się dynamicznie (np. pojawia się powiadomienie), czytnik ekranu musi o tym wiedzieć. Używamy

aria-live
:

1function MissionStatusBoard() {
2  const [status, setStatus] = useState('Oczekiwanie na start');
3  const [logs, setLogs] = useState([]);
4
5  const updateStatus = (newStatus) => {
6    setStatus(newStatus);
7    setLogs(prev => [...prev, newStatus]);
8  };
9
10  return (
11    <div>
12      {/* aria-live="polite" - czeka aż czytnik skończy bieżący tekst */}
13      <div aria-live="polite" aria-atomic="true">
14        <h2>Status misji: {status}</h2>
15      </div>
16
17      {/* aria-live="assertive" - przerywa bieżące czytanie */}
18      <div aria-live="assertive" className="sr-only">
19        {logs.length > 0 && logs[logs.length - 1]}
20      </div>
21
22      <button onClick={() => updateStatus('Odliczanie rozpoczęte')}>
23        Rozpocznij odliczanie
24      </button>
25      <button onClick={() => updateStatus('Start!')}>
26        Start
27      </button>
28    </div>
29  );
30}

Klasa sr-only (screen reader only)

Czasami potrzebujemy tekstu widocznego tylko dla czytników ekranu:

1// Styl CSS do ukrycia wizualnego, ale nie dla czytników ekranu
2const srOnlyStyle = {
3  position: 'absolute',
4  width: '1px',
5  height: '1px',
6  padding: 0,
7  margin: '-1px',
8  overflow: 'hidden',
9  clip: 'rect(0, 0, 0, 0)',
10  whiteSpace: 'nowrap',
11  borderWidth: 0,
12};
13
14function IconButton({ icon, label, onClick }) {
15  return (
16    <button onClick={onClick} aria-label={label}>
17      <span aria-hidden="true">{icon}</span>
18      <span style={srOnlyStyle}>{label}</span>
19    </button>
20  );
21}
22
23// Użycie
24<IconButton icon="🚀" label="Rozpocznij misję" onClick={startMission} />

Formularze dostępne

Formularze to jeden z najważniejszych obszarów dostępności. Każde pole musi mieć etykietę, a błędy muszą być jasno komunikowane:

1function CrewRegistrationForm() {
2  const [errors, setErrors] = useState({});
3
4  return (
5    <form aria-label="Rejestracja członka załogi">
6      <div>
7        <label htmlFor="crew-name">Imię i nazwisko:</label>
8        <input
9          id="crew-name"
10          type="text"
11          aria-required="true"
12          aria-invalid={!!errors.name}
13          aria-describedby={errors.name ? 'name-error' : undefined}
14        />
15        {errors.name && (
16          <span id="name-error" role="alert" style={{ color: 'red' }}>
17            {errors.name}
18          </span>
19        )}
20      </div>
21
22      <fieldset>
23        <legend>Specjalizacja:</legend>
24        <label>
25          <input type="radio" name="role" value="pilot" />
26          Pilot
27        </label>
28        <label>
29          <input type="radio" name="role" value="engineer" />
30          Inżynier
31        </label>
32        <label>
33          <input type="radio" name="role" value="scientist" />
34          Naukowiec
35        </label>
36      </fieldset>
37
38      <button type="submit">Dołącz do załogi</button>
39    </form>
40  );
41}

Podsumowanie

Dostępność w React to nie dodatek, ale integralna część procesu tworzenia interfejsu:

  • Semantyczny HTML to fundament - używaj
    <nav>
    ,
    <main>
    ,
    <button>
    zamiast
    <div>
    na wszystko
  • Atrybuty ARIA uzupełniają semantykę, gdy natywne elementy nie wystarczają
  • Zarządzanie fokusem jest krytyczne w SPA - modal musi "pułapkować" fokus, nawigacja musi być logiczna
  • Nawigacja klawiaturą - każdy interaktywny element musi być dostępny bez myszy
  • Live regions (
    aria-live
    ) ogłaszają dynamiczne zmiany treści
  • Formularze wymagają etykiet, opisów błędów i odpowiednich ról

Podobnie jak systemy stacji kosmicznej muszą być zaprojektowane tak, by każdy członek załogi mógł z nich korzystać w każdych warunkach - nawet gdy światła zgasną, a ekran dotykowy przestanie działać - tak dostępna aplikacja React zapewnia, że każdy użytkownik może skutecznie z niej korzystać.

Ir a CodeWorlds