İçeriğe geç
react

React'te Erişilebilirlik (a11y): WCAG Uyumlu Geliştirme

Tarık Tunç
React'te Erişilebilirlik (a11y): WCAG Uyumlu Geliştirme

React'te Erişilebilirlik (a11y): WCAG Uyumlu Geliştirme

Web erişilebilirliği, uygulamanızın engelli bireyler dahil herkes tarafından kullanılabilir olmasını sağlar. Türkiye'de Engelliler Hakkında Kanun ve dünyada WCAG (Web Content Accessibility Guidelines) standartları, web uygulamalarının erişilebilir olmasını gerektirmektedir. Bu rehberde, React uygulamalarında erişilebilirliği nasıl sağlayacağınızı pratik örneklerle adım adım ele alacağız.

Erişilebilirlik Neden Önemli?

Dünya nüfusunun yaklaşık %15'i bir tür engelle yaşamaktadır. Erişilebilir web siteleri yalnızca etik bir sorumluluk değil, aynı zamanda daha geniş bir kitleye ulaşma, SEO iyileştirme ve yasal uyumluluk açısından da kritiktir. Ayrıca erişilebilir componentler genellikle daha iyi tasarlanmış ve test edilebilir olur.

Semantik HTML ve ARIA

import { useState, useRef, useEffect, KeyboardEvent } from 'react';

// ❌ KÖTÜ: Div ile yapılmış "buton"
function BadButton() {
  return (
    <div onClick={() => console.log('click')} style={{ cursor: 'pointer' }}>
      Tıkla
    </div>
  );
  // Sorunlar: Keyboard ile erişilemez, screen reader tanımaz, focus olmaz
}

// ✅ İYİ: Semantik HTML
function GoodButton() {
  return (
    <button onClick={() => console.log('click')} type="button">
      Tıkla
    </button>
  );
  // Otomatik: keyboard erişimi, screen reader desteği, focus yönetimi
}

// Erişilebilir Modal Component
interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title: string;
  children: React.ReactNode;
}

function AccessibleModal({ isOpen, onClose, title, children }: ModalProps) {
  const modalRef = useRef<HTMLDivElement>(null);
  const previousFocus = useRef<HTMLElement | null>(null);

  useEffect(() => {
    if (isOpen) {
      // Mevcut focus'u kaydet
      previousFocus.current = document.activeElement as HTMLElement;
      // Modal'a focus ver
      modalRef.current?.focus();
      // Body scroll'u kilitle
      document.body.style.overflow = 'hidden';
    } else {
      // Eski focus'a geri dön
      previousFocus.current?.focus();
      document.body.style.overflow = '';
    }
  }, [isOpen]);

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === 'Escape') {
      onClose();
    }

    // Focus trap — Tab ile modal dışına çıkılmasını engelle
    if (e.key === 'Tab') {
      const focusableElements = modalRef.current?.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );

      if (focusableElements && focusableElements.length > 0) {
        const firstElement = focusableElements[0] as HTMLElement;
        const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;

        if (e.shiftKey && document.activeElement === firstElement) {
          e.preventDefault();
          lastElement.focus();
        } else if (!e.shiftKey && document.activeElement === lastElement) {
          e.preventDefault();
          firstElement.focus();
        }
      }
    }
  };

  if (!isOpen) return null;

  return (
    <div
      className="fixed inset-0 z-50 flex items-center justify-center"
      role="presentation"
    >
      {/* Arka plan overlay */}
      <div
        className="absolute inset-0 bg-black/50"
        onClick={onClose}
        aria-hidden="true"
      />

      {/* Modal içeriği */}
      <div
        ref={modalRef}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        tabIndex={-1}
        onKeyDown={handleKeyDown}
        className="relative bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
      >
        <h2 id="modal-title" className="text-xl font-bold mb-4">{title}</h2>
        {children}
        <button
          onClick={onClose}
          className="absolute top-4 right-4"
          aria-label="Modalı kapat"
        >
          ✕
        </button>
      </div>
    </div>
  );
}
İpucu: Her zaman semantik HTML elementlerini tercih edin. <button>, <a>, <nav>, <main>, <article> gibi elementler yerleşik erişilebilirlik özelliklerine sahiptir. ARIA attribute'ları ancak semantik HTML yeterli olmadığında kullanılmalıdır.

Erişilebilir Form Componentleri

interface FormFieldProps {
  id: string;
  label: string;
  error?: string;
  helpText?: string;
  required?: boolean;
  type?: string;
  value: string;
  onChange: (value: string) => void;
}

function AccessibleFormField({
  id, label, error, helpText, required = false, type = 'text', value, onChange,
}: FormFieldProps) {
  const errorId = `${id}-error`;
  const helpId = `${id}-help`;

  // aria-describedby birden fazla id alabilir
  const describedBy = [
    error ? errorId : null,
    helpText ? helpId : null,
  ].filter(Boolean).join(' ') || undefined;

  return (
    <div className="space-y-1">
      <label htmlFor={id} className="block text-sm font-medium">
        {label}
        {required && <span className="text-red-500 ml-1" aria-hidden="true">*</span>}
        {required && <span className="sr-only">(zorunlu alan)</span>}
      </label>

      <input
        id={id}
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        required={required}
        aria-required={required}
        aria-invalid={error ? 'true' : 'false'}
        aria-describedby={describedBy}
        className={`w-full rounded border px-3 py-2 ${error ? 'border-red-500' : 'border-gray-300'}`}
      />

      {helpText && (
        <p id={helpId} className="text-sm text-gray-500">{helpText}</p>
      )}

      {error && (
        <p id={errorId} className="text-sm text-red-500" role="alert">
          {error}
        </p>
      )}
    </div>
  );
}

Klavye Navigasyonu

import { useState, KeyboardEvent } from 'react';

interface Tab {
  id: string;
  label: string;
  content: React.ReactNode;
}

function AccessibleTabs({ tabs }: { tabs: Tab[] }) {
  const [activeIndex, setActiveIndex] = useState(0);

  const handleKeyDown = (e: KeyboardEvent, index: number) => {
    let newIndex = index;

    switch (e.key) {
      case 'ArrowRight':
        newIndex = (index + 1) % tabs.length;
        break;
      case 'ArrowLeft':
        newIndex = (index - 1 + tabs.length) % tabs.length;
        break;
      case 'Home':
        newIndex = 0;
        break;
      case 'End':
        newIndex = tabs.length - 1;
        break;
      default:
        return;
    }

    e.preventDefault();
    setActiveIndex(newIndex);
    // Yeni tab'a focus ver
    document.getElementById(`tab-${tabs[newIndex].id}`)?.focus();
  };

  return (
    <div>
      <div role="tablist" aria-label="İçerik sekmeleri" className="flex border-b">
        {tabs.map((tab, index) => (
          <button
            key={tab.id}
            id={`tab-${tab.id}`}
            role="tab"
            aria-selected={activeIndex === index}
            aria-controls={`panel-${tab.id}`}
            tabIndex={activeIndex === index ? 0 : -1}
            onClick={() => setActiveIndex(index)}
            onKeyDown={(e) => handleKeyDown(e, index)}
            className={`px-4 py-2 font-medium border-b-2 ${
              activeIndex === index
                ? 'border-blue-600 text-blue-600'
                : 'border-transparent text-gray-500'
            }`}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {tabs.map((tab, index) => (
        <div
          key={tab.id}
          id={`panel-${tab.id}`}
          role="tabpanel"
          aria-labelledby={`tab-${tab.id}`}
          hidden={activeIndex !== index}
          tabIndex={0}
          className="p-4"
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}
Uyarı: aria-label ve aria-labelledby kullanırken Türkçe karakter desteğine dikkat edin. Screen reader'lar Türkçe karakterleri genellikle doğru okur, ancak test yapmak önemlidir.

Test ve Denetleme Araçları

  • eslint-plugin-jsx-a11y: JSX'teki erişilebilirlik sorunlarını lint zamanında yakalar
  • axe-core / @axe-core/react: Runtime erişilebilirlik denetimi
  • Lighthouse: Chrome DevTools'ta yerleşik erişilebilirlik skoru
  • NVDA / VoiceOver: Screen reader ile manuel test
Bilgi: WCAG 2.1 AA seviyesi, çoğu yasal gereklilik için yeterlidir. Bu seviye; algılanabilirlik, çalıştırılabilirlik, anlaşılabilirlik ve sağlamlık olmak üzere dört temel prensibe dayanır.

Sonuç

React'te erişilebilirlik, projenin başından itibaren düşünülmesi gereken bir konudur. Semantik HTML kullanın, ARIA attribute'larını doğru uygulayın, klavye navigasyonunu destekleyin, focus yönetimini ihmal etmeyin ve renk kontrastına dikkat edin. Erişilebilirlik sadece engelli kullanıcılar için değil, tüm kullanıcıların deneyimini iyileştiren bir yaklaşımdır.

Kaynaklar

İlgili Yazılar